2020年度版はこちら
Deep Learningのフレームワークといえば,PyTorch,Tensorflow,Kerasなどたくさんの種類があります.
今回は,その中でも私がよく使わせていただいてるPyTorchに注目していこうと思います!
実はこのPyTorch,Python版だけではなく,C++版がリリースされているのはご存知でしょうか?
このおかげで,もしC++のプログラムの処理の一部としてDeep Learningを使いたいとなったときに,容易に組み込むことができるようになるのです!
そんなC++版のPyTorchですが,私は**「C++はコンパイル型言語だからもしかしたらPython版より速いのでは?」**と気になりました.
そこで,今回は実際に**「C++とPythonでどのくらい速度に差が有るのか?」**を調べてみました!
また,精度についても気になったのでついでに調べてみました.
比較実験に使用するもの
1. フレームワーク
今回は表題の通り「PyTorch」のC++版を使用します.
以下のサイトからダウンロードできるのでしてみてください!
PyTorch公式:https://pytorch.org/
私は以上のような設定でダウンロードしました.
「Preview(Nightly)版」は常に最新のファイルが置かれるようになっています.
しかし,開発途中のものであるため,安定版を使用したい場合は「Stable(1.4)」を選択しましょう.
また,一番下の「Run this Command」が結構重要で,CXXのビルドバージョンが11以上の人は下の方を選択するのをおすすめします.
現在はほとんどCXX17なので,下で大丈夫かと思います.
上を選ぶと,他のライブラリのリンクエラー等が発生して色々大変です.
2. モデル
今回は,畳み込みオートエンコーダ(Convolutional Autoencoder)を使用します.
私のGitHubから取得可能→https://github.com/koba-jon/pytorch_cpp
このモデルは,**入力画像(高次元)を潜在空間(低次元)に写像し,今度はこの潜在変数(低次元)をもとに画像(高次元)**を生成し,これと入力画像との誤差を最小化することが目的です.
学習を終えたこのモデルは,高次元の画像から低次元の空間を通って再び高次元の画像を生成できるため,学習用画像をより特徴付ける潜在空間を得ることができています.
つまり,次元圧縮の役割があり,いわゆる非線形的な主成分分析とも言えます.
これは,次元の呪いの解消,転移学習,異常検知のような様々な用途があり非常に便利です.
それでは,使用するモデルの構造について説明します.
- 画像のサイズが,1度の畳み込みで1/2倍,逆畳み込みで2倍
- 学習の安定化・収束の加速化
- 潜在変数の取り得る範囲が(-∞, +∞)
- 画素値の取り得る範囲が[-1, +1]
これらの効果を期待して,以下のネットワークを構築しました.
Operation | Kernel Size | Stride | Padding | Bias | Feature Map | BN | Activation | ||
---|---|---|---|---|---|---|---|---|---|
Input | Output | ||||||||
1 | Convolution | 4 | 2 | 1 | False | 3 | 64 | ReLU | 2 | 64 | 128 | True | ReLU |
3 | 128 | 256 | True | ReLU | |||||
4 | 256 | 512 | True | ReLU | |||||
5 | 512 | 512 | True | ReLU | |||||
6 | 512 | 512 | |||||||
7 | Transposed Convolution | 512 | 512 | True | ReLU | ||||
8 | 512 | 512 | True | ReLU | |||||
9 | 512 | 256 | True | ReLU | |||||
10 | 256 | 128 | True | ReLU | |||||
11 | 128 | 64 | True | ReLU | |||||
12 | 64 | 3 | tanh |
3. データセット
- CelebA(Large-scale CelebFaces Attributes)データセット
http://mmlab.ie.cuhk.edu.hk/projects/CelebA.html
今回は,セレブなお方の顔画像(カラー)を202,599枚集めたデータセットであるCelebAデータセットを使用します.
画像サイズが178×218[pixel]と,逆畳み込みの際に少々不都合が生じるため,今回は64×64[pixel]にリサイズしました.
また,その内9割(182,340枚)を学習用画像に,1割(20,259枚)をテスト用画像に使用しました.
これを先ほどのモデルに入力すると,潜在空間は(C,H,W)=(512,1,1)となります.もし,128×128[pixel]またはそれ以上の画像を入力した場合,中間層はSpatialな潜在空間になります.
比較対象
今回は,**「C++とPythonでどの程度速度が異なるのか」**を調査するのがメインですが,以下の5種類の環境下での速度および性能も加えて比較していきたいと思います.
- CPUメイン稼働
- Python
- C++
- GPUメイン稼働
- Python
- 非決定論的
- 決定論的
- C++
- Python
1. メインで稼働させるユニットの違い(CPU or GPU)
- CPU
「直列的で」「複雑な」命令を処理するのが得意 - GPU
「並列的で」「単純な」命令を処理するのが得意
以上の特徴から分かる通り,画像を扱うDeep Learningでは,計算速度において圧倒的にGPUが有利であることが分かります.
(1) Pythonによる実装
- CPUを使用する場合
device = torch.device('cpu') # CPUを使用
model.to(device) # モデルをCPUに移す
image = image.to(device) # データをCPUに移す
- GPUを使用する場合
device = torch.device('cuda') # デフォルトのGPUを使用
device = torch.device('cuda:0') # 1番目のGPUを使用
device = torch.device('cuda:1') # 2番目のGPUを使用
model.to(device) # モデルをGPUに移す
image = image.to(device) # データをGPUに移す
(2) C++による実装
- CPUを使用する場合
torch::Device device(torch::kCPU); // CPUを使用
model->to(device); // モデルをCPUに移す
image = image.to(device); // データをCPUに移す
- GPUを使用する場合
torch::Device device(torch::kCUDA); // デフォルトのGPUを使用
torch::Device device(torch::kCUDA, 0); // 1番目のGPUを使用
torch::Device device(torch::kCUDA, 1); // 2番目のGPUを使用
model->to(device); // モデルをGPUに移す
image = image.to(device); // データをGPUに移す
2.決定論的か否かの違い(GPUメイン稼働&Pythonに限る)
Python版のPyTorchには,GPUを用いた学習の場合,cuDNNを用いて学習の速度を向上させるという操作がされています.
しかし,C++とは異なり,学習の速度が向上するからと言って,再び学習を回したら必ずしも全く同じ状況を再現できるとは限りません.
そこで,PyTorch公式は,再現性を担保するためには以下のようにcuDNNの挙動を決定論的にする必要があり,それと同時に速度が低下すると明言しています.
Deterministic mode can have a performance impact, depending on your model. This means that due to the deterministic nature of the model, the processing speed (i.e. processed batch items per second) can be lower than when the model is non-deterministic.
エンジニアの立場においては,再現性の有無について気にする場合があるため,また再現性の有無によって速度に変化が生じるため,今回の速度の比較に含めました.
C++の「rand」関数とは異なり,乱数の初期値を何も設定しないとランダムになってしまうので,Pythonで再現性を担保するためには,明示的に乱数の初期値を設定する必要があります.
(乱数の初期値の設定が速度に影響を与えることはありません.)
実装については以下の通りです.
- 決定論的な場合
seed = 0
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
np.random.seed(seed)
random.seed(seed)
torch.backends.cudnn.deterministic = True # 速度が低下する代わりに決定論的
torch.backends.cudnn.benchmark = False # 速度が低下する代わりに決定論的
- 非決定論的な場合
torch.backends.cudnn.deterministic = False # 非決定論的である代わりに高速化
torch.backends.cudnn.benchmark = True # 画像サイズが変化しない場合に高速化
3. プログラミング言語間の実装の違い
実装したい内容自体が同じでもプログラミング言語が変わることで,記法やルールが変わったり,必要なライブラリが変わったり,ということが発生します.
PythonとC++は共にオブジェクト指向言語なので概念自体は似ていますが,何よりPythonはインタプリタ型,C++はコンパイル型なので,C++に動的型付けが通用しないことを踏まえながら実装しなければなりません.
また,PyTorchのC++ APIは現在発展途上であるため,一部の機能が整備されてないことも考慮しなければいけません.
これらの点を踏まえ,Python,C++間での実装の違い,および私が実装したプログラムについて紹介します.
(1) ライブラリの使用状況
現在一般的に書かれているPythonのライブラリの使用状況に加え,C++で実装する場合に推奨するライブラリ,および私が実際に書いたプログラムのライブラリの使用状況についても記載します.
Python(推奨) | C++(推奨) | C++(自作) | |
---|---|---|---|
コマンドライン引数の処理 | argparse | boost::program_options | boost::program_options |
モデルの設計 | torch.nn | torch::nn | torch::nn |
前処理(transform) | torchvision.transforms | torch::data::transforms(実行前に多彩な前処理する場合) or 自作(実行後に多彩な前処理する場合) |
自作(OpenCV使用) |
データセットの取得(datasets) | torchvision.datasets(Pillow使用) | 自作(OpenCV使用) | 自作(OpenCV使用) |
データローダー(dataloader) | torch.utils.data.DataLoader | torch::data::make_data_loader(クラス分類の場合) or 自作(クラス分類以外の場合) |
自作(OpenMP使用) |
損失関数(loss) | torch.nn | torch::nn | torch::nn |
最適化手法(optimizer) | torch.optim | torch::optim | torch::optim |
誤差逆伝搬法(backward) | torch.Tensor.backward() | torch::Tensor::backward() | torch::Tensor::backward() |
プログレスバー | tqdm | boost | 自作 |
**現時点(2020/03/24)**では,以上のような感じになります.
PyTorchのライブラリをC++で使う場合は,クラス名や関数名がPythonとほとんど同じです.
これは,製作者側がユーザーに配慮してのことだそうです.非常にありがたいですね!
次に,C++でPyTorchのプログラムを書く際に,特に気をつけたほうが良い点について記載します.
(2) モデルの設計
以下,私が書いたプログラムの一部を抜粋して記載します.
using namespace torch;
namespace po = boost::program_options;
struct ConvolutionalAutoEncoderImpl : nn::Module{
private:
nn::Sequential encoder, decoder;
public:
ConvolutionalAutoEncoderImpl(po::Variables_map &vm);
torch::Tensor forward(torch::Tensor x);
}
TORCH_MODULE(ConvolutionalAutoEncoder);
モデルを設計する際は,Python同様に「torch::nn」クラスを使用します.
また,モデルを作成する際は構造体を使用します.(クラスのバージョンもありますが,少し複雑になるっぽい)
この際に注意するべきことがPython同様に,**nn::Moduleを継承する**ということです.
ここについては,Pythonの書き方と同じですね.
次に重要なのが,構造体の名前を「[モデル名]Impl」にし,構造体の下に「TORCH_MODULE([モデル名])」を追加 することです.
これをしないと,モデルを保存したり,読み込んだりすることができなくなります.
また,「TORCH_MODULE([モデル名])」とすることで普通の構造体「ConvolutionalAutoEncoderImpl」をモデル用の構造体「ConvolutionalAutoEncoder」として宣言できるようになりますが,おそらく内部でクラスの継承をさらにおこなっている?(予想)ため,メンバ変数にアクセスする場合は「model->to(device)」のように,**「->」(アロー演算子)**を使う必要があるので注意してください.
次に,上記の件に関連しますが,nnクラスのモジュールを使用する際の注意点について説明します.
Python同様に「nn::Sequential」を使用することができます.C++で「nn::Sequential」にモジュールを追加していくためには,vector型のように**「push_back」を使います.
ここで,「push_back」関数を呼ぶためには,「->」(アロー演算子)**を使用することに気をつけてください.
実装例は以下のような感じです.
nn::Sequential sq;
sq->push_back(nn::Conv2d(nn::Conv2dOptions(3, 64, /*kernel_size=*/4).stride(2).padding(1).bias(false)));
sq->push_back(nn::BatchNorm2d(64));
sq->push_back(nn::ReLU(nn::ReLUOptions().inplace(true)));
(3) transform・datasets・dataloaderの自作
transform,datasets,dataloaderを自作する上では,テンソル型のデータを他の変数に渡す際に**「.clone()」を使って渡す**ことです.私はここでハマりました.
テンソル型は計算グラフを扱う関係で?(予想),このように設定しないとテンソル内の値が変わる可能性があります.
void transforms::Normalize::forward(torch::Tensor &data_in, torch::Tensor &data_out){
torch::Tensor data_out_src = (data_in - this->mean) / this->std;
data_out = data_out_src.clone();
return;
}
(4) その他のプログラム
その他のプログラムについては,ほとんどPython版と同じでハマるところは特にありません.
また,Python版とは異なる部分で少し使いにくいなと思ったクラスは自作しました.
具体的なプログラムは,以下のGitHubから見れますので参考にしてください.
https://github.com/koba-jon/pytorch_cpp/tree/master/Dimensionality_Reduction/AE2d
もしかしたら,ソースコードの解説記事も書くかもしれません.
もし,「ここがおかしい」という意見がありましたら,大歓迎ですので是非コメントしてください.
プログラミング言語間で統一させた項目
基本的には,Pythonで有るライブラリがC++には無いといったように,どうしようもない箇所以外は,ほとんど同じと思ってもらって構いません.
また,GitHubのプログラムから変えてないと思ってもらって結構です.
具体的には,Python版とC++版を比較する上で,以下の内容を統一させました.
- 画像サイズ(64×64×3)
- 画像の種類(学習方法Aの画像群=学習方法Bの画像群)
- バッチサイズ(16)
- 潜在空間のサイズ(1×1×512)
- 最適化手法(Adam,learning rate=0.0001,β1=0.5,β2=0.999)
- モデルの構造
- モデルの初期化方法
- 畳み込み層,逆畳み込み層:平均0.0,標準偏差0.02
- バッチノーマライゼーション:平均1.0,標準偏差0.02
- データのロード方法
- 「datasets」クラスの初期化時にはパスのみ取得し,実際に動作させる時に初めてパスをもとに画像を読み込む.
- 「datasets」クラスの稼働時に,1組のデータ(1枚の画像と1個のパス)のみを読み込む.
- 「datasets」クラスの稼働時に,「transform」を実行する.
- 「DataLoader」クラスの稼働時に,並列的にミニバッチのデータを「datasets」クラスから読み込む.
- データセットのシャッフル方法
- 学習時はシャッフルするが,推論時はシャッフルしない.
- エポックごとに,データを入力する一番最初にシャッフルする.
実験結果
比較する各対象において,celebAの64×64の画像182,340枚を用いて,L1誤差を最小化するように,1[epoch]だけ畳み込みオートエンコーダモデルをミニバッチ学習させました.
その際の**「1[epoch]当たりの時間」および「GPUのメモリ使用量」**を調べました.
ここで,「1[epoch]当たりの時間」はtqdmや自作した関数の処理時間も含まれています.
これについては,それが合計の処理時間にほとんど影響を与えなかったという点と,実際にPyTorchを使うときはビジュアライズもあったほうが便利だから使う人が多い点から含めました.
また,その学習済みモデルを用いて,テスト画像20,259枚を使って1枚ずつモデルに入力し,テストしました.
その際の**「順伝搬の平均速度」および「入力画像と出力画像のL1誤差」**も調べました.
そして,「実行ファイル」と「nvidia-smi」以外のものは一切立ち上げずに(Ubuntu起動時に最初から稼働しているものはそのまま),学習・テストしました.
CPU(Core i7-8700) | GPU(GeForce GTX 1070) | |||||
---|---|---|---|---|---|---|
Python | C++ | Python | C++ | |||
非決定論的 | 決定論的 | |||||
学習 | 時間[time/epoch] | 1時間04分49秒 | 1時間03分00秒 | 5分53秒 | 7分42秒 | 17分36秒 |
GPUメモリ[MiB] | 2 | 9 | 933 | 913 | 2941 | |
テスト | 速度[seconds/data] | 0.01189 | 0.01477 | 0.00102 | 0.00101 | 0.00101 |
L1誤差(MAE) | 0.12621 | 0.12958 | 0.12325 | 0.12104 | 0.13158 |
C++はコンパイル型言語です.
したがって,インタプリタ型言語のPythonに勝つ...かと思いきやどちらもいい勝負でした.
学習時間に関しては,CPUはほとんど同じ,GPUはC++がPythonより2倍以上遅いことがわかりました.(なぜ?)
この結果ですが,CPUは同じくらいでGPUのときだけ大きく異なるので,
- PyTorch C++版のGPUにおける処理の整備が完璧になっておらず,GPUによる順伝搬,逆伝搬が最適化されていない可能性
- CPUで得たミニバッチのデータをGPUに移す際に時間がかかっている可能性
が挙げられそうです.
以下の方が実験されているように,やはり**GPUメイン稼働の場合はPythonのほうが速い**という結果は間違いなさそうです.
https://www.noconote.work/entry/2019/01/11/151624
また,推論(テスト)の速度や性能についてもPythonとほとんど変化ないため,**現状はPythonのほうが良い**かもしれません.
GPUのメモリ使用量もなぜか多いですね.(ReLUのinplaceをTrueにしてるのに...)
Python(GPU)の決定論的・非決定論的の結果ですが,公式が明言している通り決定論的のほうが遅くなりました.
やはり,ここの時間は変わりますね.
結論
-
学習速度
-
1位:Python版(非決定論的,GPUメイン稼働)
-
2位:Python版(決定論的,GPUメイン稼働)
-
3位:C++版(GPUメイン稼働)
-
4位:CPUメイン稼働(Python版,C++版 同程度)
-
推論速度
-
1位:GPUメイン稼働(Python版,C++版 同程度)
-
2位:CPUメイン稼働(Python版,C++版 同程度)
-
性能
-
どれも同程度
おわりに
今回はPyTorchのPython版とC++版で,速度と性能を比較しました.
その結果,性能においてはPythonとC++はほとんど変わらないため,C++のPyTorchを使っても問題ないと思いました.
しかし,**現段階では,速度を求めてC++のPyTorchをやるというのは,あまりオススメできない**かもしれません.
もしかしたら,C++のAPIはまだ発展途上であるため,今後大幅に改善されるかもしれませんね!
今後に期待です!