はじめに
深層学習フレームワークPyTorchのインターフェースはPythonであるが、バックエンドはC++である。現在、C++のインターフェース(C++11)も整備されつつある。今回から3回に分けてPyTorch C++(LibTorch 1.2)による実装例の解説を行う。
なぜC++を使うのか?
主な理由は以下になるだろう。
- Pythonを使えない環境で深層学習を行う必要があるから。
- 実時間性がクリティカルになる環境で深層学習を行う必要があるから。
- マルチスレッド環境で深層学習を行う必要があるから。Pythonは、GIL(Global Interpreter Lock)のため2つ以上のスレッドを同時に走らせることができない。
- C++で実装された既存システムとシームレスに接続し深層学習を行う必要があるから。
個人的な理由は以下の通りである。
- 私のホームグランドがC++であるから。
- Pythonばかりの日常に少し飽きてきたから。
インストール方法
ここの通りにすれば良い。実装したプログラムのコンパイルにはcmakeを使うので、あらかじめこれをインストールしておく必要がある。今回のプログラムの動作確認は、EC2インスタントの1つであるAWS深層学習AMI上で行った。
やりたいこと
データセットCIFAR10を用いた10分類問題である。具体的には、この本の2章にあるKeras実装例を書き換え、以下のようなコマンドラインアプリを作る。GPU上でも動作するアプリである。
1 2 3 4 5 6 7 8 9 |
./classify_cifar10 \ --batch_size 32 \ --epochs 20 \ --resume false \ --model_path /home/ubuntu/data/foster/ch02_01/model_e10.pt \ --opt_path /home/ubuntu/data/foster/ch02_01/opt_e10.pt \ --verbose true \ --trained_model_path /home/ubuntu/data/foster/ch02_01/model_e10.pt \ --trained_opt_path /home/ubuntu/data/foster/ch02_01/opt_e10.pt \ |
引数の意味は以下の通り。
- –batch_size:バッチサイズを指定する。
- –epochs:エポック数を指定する。
- –resume:訓練済みモデルを用いて学習を途中から再開したいときはtrueを指定する。
- –model_path:訓練後のモデルを保存するパスを指定する。
- –opt_path:訓練後の最適化器を保存するパスを指定する。
- –verbose:ログ出力する場合はtrueを指定する。
- –trained_model_path:訓練済みモデルへのパスを指定する。
- –trained_opt_path:訓練済み最適化器へのパスを指定する。
最後の2つのパスは、–resumeがtrueのときに使う。
コード説明
次の目次に沿って説明する。全ソースはここにある。
- コード全体の概説
- 引数の抽出
- デバイスの選択
- モデルの定義
- データセットの読み込み
- 最適化器の準備
- 訓練済みモデルのロード
- モデルの訓練
- モデルの保存
今回は、最初の「コード全体の概説」を行う。それ以外の項目については次回以降に説明する。
コード全体の概説
関数main
の中身は以下の通り(main.cpp)。
- 5行目から20行目まででコマンドラインの引数を抽出する。
- 24行目の関数
manual_seed
で乱数の種を固定する。 - 25行目から36行目まででGPUが使えるかを判断する。
- 40行目でネットワークを定義する。
- 41行目の
to
メソッドでモデルをGPUデバイス側へ転送する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
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("batch_size", "invalid batch size", vm); const auto epochs = extract_parameter("epochs", "invalid epochs", vm); const auto resumes = extract_parameter("resume", "invalid resume", vm); const auto model_path = extract_parameter("model_path", "invalid path", vm); const auto opt_path = extract_parameter("opt_path", "invalid path", vm); const auto verbose = extract_parameter("verbose", "invalid verbose", vm); //_/_/_/ 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}; //_/_/_/ Define a model Architecture model{IMAGE_WIDTH * IMAGE_HEIGHT * IMAGE_CHANNELS, CLASSES}; model->to(device); print_parameters(model); //_/_/_/ Load the Data const auto train_dataset = load_dataset(TRAIN_PATHS). map(torch::data::transforms::Normalize<>(0, 255.0)). map(torch::data::transforms::Stack<>()); const size_t train_dataset_size = train_dataset.size().value(); const auto test_dataset = load_dataset(TEST_PATHS). map(torch::data::transforms::Normalize<>(0, 255.0)). map(torch::data::transforms::Stack<>()); const size_t test_dataset_size = test_dataset.size().value(); const auto train_loader = torch::data::make_data_loader( std::move(train_dataset), torch::data::DataLoaderOptions().batch_size(batch_size).workers(2)); const auto test_loader = torch::data::make_data_loader( std::move(test_dataset), torch::data::DataLoaderOptions().batch_size(batch_size).workers(2)); //_/_/_/ Configure a optimizer torch::optim::Adam optimizer { model->parameters(), torch::optim::AdamOptions(LEARNING_RATE) }; //_/_/_/ Train the model if (resumes) { std::cout << "resume training!" << std::endl; const auto trained_model_path = extract_parameter("trained_model_path", "invalid path", vm); const auto trained_opt_path = extract_parameter("trained_opt_path", "invalid path", vm); torch::load(model, trained_model_path); torch::load(optimizer, trained_opt_path); } const auto start = std::chrono::system_clock::now(); for (auto epoch = 1; epoch <= epochs; ++epoch) { train(epoch, model, device, *train_loader, optimizer, train_dataset_size, verbose); test(model, device, *test_loader, test_dataset_size, verbose); } const auto end = std::chrono::system_clock::now(); std::cout << std::chrono::duration_cast(end - start).count() << " [sec]" << std::endl; //_/_/_/ Save the model torch::save(model, model_path); torch::save(optimizer, opt_path); } |
- 46行目から62行目まででデータセットを読み込む。
- 66行目から69行目までで最適化器を定義する。
- 73行目から80行目までで、訓練済みモデルが指定されていればこれを読み込む。
- 83行目から87行目まででモデルを訓練(関数
train
)・評価(関数test
)する。 - 93行目からの2行でモデルを保存する。
バッチサイズ32、エポック数10のときの訓練時間は26秒程度となった。一方、Kerasのコードでの訓練時間は70秒程度である。どちらの実行でもログ出力をオフにして計測を行った。PyTorch C++版の方が2倍以上速いことが分かる。Kerasのコードでは学習時に関数fit
を呼び出している。この関数はマルチプロセスを実現する引数workers
を持たない。一方、今回示したC++の実装では、データローダ作成時にworkers
に2を渡し、マルチスレッドによるデータ供給を行わせた。この違いが速度に現れているのかもしれない。テストデータに対する精度は、Keras版が0.5032、PyTorch C++版が0.506なので差はほとんどない。乱数で10分類したときの精度(0.1)よりは5倍ほど良い。
まとめ
今回はコードの全体像と計算時間、精度について説明し、PyTorch C++版の方がKeras版より高速に処理されることを見た。バックエンドではどちらもGPUを使うので、純粋にCPU側の差であろう。ただし、上で見たように、現在のコードでKeras版とC++版の速度を比較することはフェアでない。実は同じ本の3章にあるCNNを含むネットワークもPyTorch C++で実装したのだが、Keras版と速度的には互角であった。バックエンドにGPUを使う場合、訓練速度の優劣を評価するのはナンセンスなのかもしれない。
次回以降で、上に掲げた目次に沿ってコードの中身を説明する。