はじめに
深層学習フレームワークPyTorchのインターフェースはPythonであるが、バックエンドはC++である。現在、C++のインターフェース(C++11)も整備されつつある。前回からPyTorch C++(LibTorch 1.2)による実装例の解説を行っている。今回は第2回目である。
コードの説明
本シリーズの目次は以下の通り。今回はこの内2,3,4を説明する。全ソースはここにある。
- コード全体の概説
- 引数の抽出
- デバイスの選択
- モデルの定義
- データセットの読み込み
- 最適化器の準備
- 訓練済みモデルのロード
- モデルの訓練
- モデルの保存
引数の抽出
関数main
内の該当部分は以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
int main(int argc, const char* argv[]) { //_/_/_/ Extract arguments auto desc = parse_arguments(); po::variables_map vm {}; po::store(parse_command_line(argc, argv, desc), vm); po::notify(vm); if (vm.empty() || vm.count("help")) { std::cout << desc << std::endl; return 1; } const auto batch_size = extract_parameter<int>("batch_size", "invalid batch size", vm); const auto epochs = extract_parameter<int>("epochs", "invalid epochs", vm); const auto resumes = extract_parameter<bool>("resume", "invalid resume", vm); const auto model_path = extract_parameter<std::string>("model_path", "invalid path", vm); const auto opt_path = extract_parameter<std::string>("opt_path", "invalid path", vm); const auto verbose = extract_parameter<bool>("verbose", "invalid verbose", vm); |
関数parse_arguments
の中身は以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
po::options_description parse_arguments() { po::options_description desc {"ch02_01"}; desc.add_options() ("help", "produce help messsage") ("batch_size", po::value<int>(), "set batch size") ("epochs", po::value<int>(), "set epochs") ("resume", po::value<bool>()->default_value(false), "set true or false") ("model_path", po::value<std::string>(), "set a path to the model") ("opt_path", po::value<std::string>(), "set a path to the optimizer") ("trained_model_path", po::value<std::string>(), "set a path to the trained model") ("trained_opt_path", po::value<std::string>(), "set a path to the trained optimizer") ("verbose", po::value<bool>()->default_value(false), "set true or false") ; return desc; } |
名前空間po
はboost::program_options
の言い換えである。コマンドライン引数を扱う処理をboostを用いて実装した。
デバイスの選択
関数main
内の該当部分は以下の通り。GPUを使える環境であればGPUを使うよう設定する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//_/_/_/ Select device torch::manual_seed(1); torch::DeviceType device_type{}; if (torch::cuda::is_available()) { std::cout << "CUDA available! Training on GPU." << std::endl; device_type = torch::kCUDA; } else { std::cout << "Training on CPU." << std::endl; device_type = torch::kCPU; } torch::Device device{device_type}; |
3行目のコードはプログラム全体で使う乱数の種を固定する処理なので、GPU云々とは関係ない。
モデルの定義
関数main
内の該当コードは以下の通り。
1 2 3 4 5 |
//_/_/_/ Define a model Architecture model{IMAGE_WIDTH * IMAGE_HEIGHT * IMAGE_CHANNELS, CLASSES}; model->to(device); print_parameters(model); |
今回使うネットワークは3層の全結合層から構成される単純なものである。3行目で、クラスArchitecture
のコンストラクタを呼び出している。これの第1引数は画像を1次元ベクトルに変換したときの次元数、第2引数は分類数(10)である(今回考える問題はCIFAR10を用いた画像の10分類問題である)。4行目でGPUデバイスのメモリにモデルを転送する。3行目でmodel
を値として定義しているのに、関数to
にアクセスする際になぜ演算子->
を用いているのか疑問に思うかもしれない。この種明かしは後述する。5行目の関数は学習で決定されるパラメータ数を表示する。その定義は以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void print_parameters(const Architecture& model) { int s {0}; for (const auto& pair : model->named_parameters()) { const auto& key = pair.key(); const auto& value = pair.value(); //<< ": " << pair.value().sizes() << std::endl; auto c = 1; for (const auto& v : value.sizes()) { c *= v; } std::cout << key << ": " << pair.value().sizes() << " -> " << c << std::endl; s += c; } std::cout << "total number of parameters: " << s << std::endl; |
model->named_parameters()
によりモデル内の全層のパラメータに階層的にアクセスすることができる。この出力は以下の通り。
1 2 3 4 5 6 7 |
dense1_.weight: [200, 3072] -> 614400 dense1_.bias: [200] -> 200 dense2_.weight: [150, 200] -> 30000 dense2_.bias: [150] -> 150 dense3_.weight: [10, 150] -> 1500 dense3_.bias: [10] -> 10 total number of parameters: 646260 |
CIFAR10の画像サイズは32×32である。RGBの3チャンネルあるから1次元ベクトルに変換した時の次元数は32x32x3=3072になる。これを第1層で200次元のベクトルに変換する。第2層では150次元に、第3層で10次元のベクトルに変換する。学習で決まる総パラメータ数は614400+200+30000+150+1500+10=646260である。
ネットワークを定義するクラスは以下の通り(architecture.h)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class ArchitectureImpl : public torch::nn::Module { public: ArchitectureImpl(int in_feature, int out_features); ArchitectureImpl(const ArchitectureImpl&) = delete; ArchitectureImpl& operator=(const ArchitectureImpl&) = delete; torch::Tensor forward(torch::Tensor x); private: torch::nn::Linear dense1_; torch::nn::Linear dense2_; torch::nn::Linear dense3_; }; TORCH_MODULE(Architecture); |
ネットワークのアーキテクチャはtorch::nn::Module
をpublic継承して実装しなければならない。そして、入力値x
を処理する関数forward
を実装する(8行目)。関数名に縛りはないがforwardにしておくのが無難である。今回考えるネットワークは3つの全結合層から構成される。従って、これらをインスタンス変数として定義する(11行目から13行目)。全結合層はクラスtorch::nn::Linear
として提供されている。
次に、16行目の記述TORCH_MODULE(Architecture)
を説明する。これは、PyTorch C++のソースコードtorch/csrc/api/include/torch/nn/pimpl.hで定義された次のマクロである。
1 |
#define TORCH_MODULE(Name) TORCH_MODULE_IMPL(Name, Name##Impl) |
この記述には別のマクロTORCH_MODULE_IMPL
が書かれている。その定義もpimpl.hに記載されている。
1 2 3 4 5 |
#define TORCH_MODULE_IMPL(Name, Impl) \ class Name : public torch::nn::ModuleHolder<Impl> { /* NOLINT */ \ public: \ using torch::nn::ModuleHolder<Impl>::ModuleHolder; \ } |
つまり、TORCH_MODULE(Architecture);
は以下のように展開される。
1 2 3 4 |
class Architecture : public torch::nn::ModuleHolder<ArchitectureImpl> { public: using torch::nn::ModuleHolder<ArchitectureImpl>::ModuleHolder; }; |
ここで、torch::nn::ModuleHolderは以下で定義されるテンプレートクラスである。
1 2 3 4 5 6 7 8 |
template <typename Contained> class ModuleHolder : torch::detail::ModuleHolderIndicator { protected: /// The module pointer this class wraps. /// NOTE: Must be placed at the top of the class so that we can use it with /// trailing return types below. std::shared_ptr<Contained> impl_; ..... |
テンプレート引数Contained
にArchitectureImpl
が渡されるので、7行目でstd::shared_ptr<ArchitectureImpl>
が定義されることになる。以上をまとめると TORCH_MODULE(Architecture)
と言う記述により、クラスArchitecureImpl
のインスタンスがスマートポインタにより所有者管理されるクラスArchitecture
が自動生成されることになる。クラスArchitecture
には、そのインスタンスをポインタライクに扱うための関数が定義されているので、先に触れた構文mode->to(device)
が書かれることになる。このTORCH_MODULE
を用いた仕組みは、PyTorch C++の主要なクラスに踏襲されており、全結合層のクラスtorch::nn::Linear
も例外ではない。
ここで1つ注意すべき点がある。ユーザが定義したクラス(ここではArchitectureImpl
)にデフォルトコンストラクタが存在しない場合、以下のコードはコンパイルエラーになる。
1 |
Architecture model{}; |
なぜなら、上のコードは内部でstd::make_shared<ArchitectureImpl>()
を実行しており、これはArchitectureImpl
にデフォルトコンストラクタがない場合エラーを返すためである。空のインスタンスを作りたい場合は以下のようにすれば良い。
1 |
Architecture model {nullptr}; |
クラスModuleHolder
には上の構文を受け付けるコンストラクタが用意されている。
クラスArchitectureImpl
の実装部分(architecture.cpp)は以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
ArchitectureImpl::ArchitectureImpl(int in_features, int out_features) : dense1_{in_features, N_HIDDEN1} , dense2_{N_HIDDEN1, N_HIDDEN2} , dense3_{N_HIDDEN2, out_features} { register_module("dense1_", dense1_); register_module("dense2_", dense2_); register_module("dense3_", dense3_); } torch::Tensor ArchitectureImpl::forward(torch::Tensor x) { x = torch::flatten(x, 1); // [batch_size, in_features] x = torch::relu(dense1_->forward(x)); // [batch_size, N_HIDDEN1] x = torch::relu(dense2_->forward(x)); // [batch_size, N_HIDDEN2] x = dense3_->forward(x); // [batch_size, out_features] return torch::log_softmax(x, 1); } |
ここで注意すべき点は、訓練すべき層をregister_module
で明示的に「登録」することである(6行目から8行目)。これにより、内部で使うパラメータやバッファなどに階層的にアクセスすることが可能となり、全層の重みに対して誤差逆伝播が可能となる。関数forward
の処理内容は以下の通りである。
flatten
:入力値x
のサイズ[batch_size, channels, rows, cols]を[batch_size, channels * rows * cols]に変換する。- 第1層の出力に活性化関数
ReLU
を適用する。 - 第2層の出力に活性化関数
ReLU
を適用する。 - 第3層の出力に活性化関数
Softmax
を適用し対数をとる。
まとめ
今回は、目次の項目2,3,4を説明した。PyTorch C++の主要なクラスはstd::shared_ptr
で管理されること、学習される重みを持つインスタンスはregister_module
を使って明示的に登録する必要があることを説明した。次回も引き続きPyTorch C++の説明を行う。