PyTorchでDeepPoseを実装してみた PartⅡ

# はじめに
前回の記事([PyTorchでDeepPoseを実装してみた](http://qiita.com/ynaka81/items/85659dff4d1c2c593f21))ではDeepPoseを実装しつつ，ChainerとPyTorchを比較しました．PyTorchはChainer同様に実装しやすく，性能面では予測はChainerと同程度，学習はChainerよりも早いという結果になりました．今回は性能面について，前回やり残した調査・検証を行い深堀りしていきます．


# 前回からの実装変更点
前回，PyTorchの学習速度の方がChainerよりも早いことについて，PyTorchではLoss関数のbackward計算の自動微分がCで(ネイティブに)実行されるためではないかという仮設を立てました．今回はそれを検証するために2点，実装を変更しました．
1. 乱数の固定
2. backwardの明示的実装
乱数を固定するのは検証の再現性を高めるため，backwardを明示的に実装するのはそれによる学習速度の影響を見るためです．

## 乱数の固定
学習をスタートさせる前に乱数シードを指定する処理を追加しました．なお，Chainerの`iterator`は`MultiprocessIterator`を使っており，マルチプロセスでの乱数固定はしんどかったので，イテレーション内のData Augmentationは無効にしています．

*Chainer*

```py
     def start(self):
         """ Train pose net. """
+        # set random seed.
+        if self.seed is not None:
+            random.seed(self.seed)
+            np.random.seed(self.seed)
+            if self.gpu >= 0:
+                chainer.cuda.cupy.random.seed(self.seed)
         # initialize model to train.
         model = AlexNet(self.Nj, self.use_visibility)
```

*PyTorch*

```py
     def start(self):
         """ Train pose net. """
+        # set random seed.
+        if self.seed is not None:
+            random.seed(self.seed)
+            torch.manual_seed(self.seed)
+            if self.gpu:
+                torch.cuda.manual_seed(self.seed)
         # initialize model to train.
         model = AlexNet(self.Nj)
```

## backwardの明示的実装
[Extending PyTorch](http://pytorch.org/docs/notes/extending.html)によれば，PyTorchでは，`Module`の微分は自動微分で，`Function`の微分は要実装という棲み分けのようです．なので，今回は`Function.backward`を実装すればよさそうです．また，`Module`の入力は`Variable`で，`Function`の入力は`Tensor`である点には注意が必要です．
なお，`Function`には`save_for_backward`という便利メソッドがあって，`backward`用に変数を取っておけるのですが，計算途中の値についてはサポートしておらず，`backward`計算に用いる`forward`計算の途中結果はメンバ変数に格納しました．

*PyTorch*

```py
     def forward(self, *inputs):
         x, t, v = inputs
-        diff = x - t
+        self.diff = x - t
         if self.use_visibility:
-            N = (v.sum()/2).data[0]
-            diff = diff*v
+            self.N = v.sum()/2
+            self.diff = self.diff*v
         else:
-            N = diff.numel()/2
-        diff = diff.view(-1)
-        return diff.dot(diff)/N
+            self.N = self.diff.numel()/2
+        diff = self.diff.view(-1)
+        return torch.Tensor([diff.dot(diff)/self.N])
+
+    def backward(self, *grad_outputs):
+        coeff = grad_outputs[0][0]*2/self.N
+        gx0 = coeff*self.diff
+        return gx0, None, None
```


# 検証
前回立てた仮設を検証するため追加で実験を行いました．
なお，検証に用いたデータセット，環境は前回と同じです．

## 自動微分の影響
PyTorchの自動微分がCで(ネイティブに)実行されることによる学習速度への影響を検証するため，PyTorchの自動微分と明示的微分について，CPU環境とGPU環境でそれぞれ100epoch学習させた時の所要時間を計測しました．

### CPU環境
CPU環境では，PyTorchの自動微分も明示的微分も学習時間はほぼ変わらないという結果になりました．

| ライブラリ          | 所要時間 [h] |
|:-------------------|------------:|
| PyTorch(自動微分)   | 47.6        |
| PyTorch(明示的微分) | 47.6        |

乱数を固定しているため，PyTorchの学習曲線はほぼ重なりました．
![Training_time_comparison_(CPU).png](https://qiita-image-store.s3.amazonaws.com/0/130762/fce4e955-25f3-53a5-40f7-b6a85414d846.png)

### GPU環境
GPU環境では，学習時間はPyTorchの自動微分の方が明示的微分よりもやや遅いという結果になりました．Python実装の方がはやい点は理解が難しいですが，GPUのランダム性に起因する可能性もあり，何度か試すことで結果は変わるかも分かりません．

| ライブラリ          | 所要時間 [h] |
|:-------------------|------------:|
| PyTorch(自動微分)   | 2.60        |
| PyTorch(明示的微分) | 2.49        |

乱数は固定していますが，GPU起因のランダム性があるため，PyTorchの学習曲線は実装方法により時間軸方向にズレが見れます．
![Training_time_comparison_(GPU).png](https://qiita-image-store.s3.amazonaws.com/0/130762/2e020b47-5a51-5d01-4661-9b738d9f805f.png)

## ネットワークレイヤー実装の影響
上記の実験結果を見るに，前回立てた仮設は必ずしも正解ではなさそうです．原因を調査するため，再度コードを眺めていると，ChainerではConvolutionなど各層の実装がPythonであり，PyTorchではCでした．
これが学習時間に対して支配的な影響を与えそうということで，ChainerとPyTorchでLoss関数(ネットワーク込)のforwardとbackward計算に要する合計時間を計測しました．今回は$2^n$のバッチサイズに対して，それぞれ100回計測し平均と分散を計算しました．

### CPU環境
CPU環境では$n=0$の場合は，ほぼ同様な処理時間ですが，$n$の増加に従いPyTorchの方が優位になるという結果になりました．CPU環境でのChainer，PyTorchの1epoch平均の学習時間がそれぞれ8.2[sec]，5.0[sec]であることを踏まえると，上記仮設は妥当そうです．
![Core_process_time_comparison_(CPU).png](https://qiita-image-store.s3.amazonaws.com/0/130762/6b0eea3a-ef41-9756-0cb6-42b3f5d061f4.png)

### GPU環境
GPU環境でも，CPU環境と同様，$n$の増加に従いPyTorchの方が優位になるという結果になりました．GPU環境でのChainer，PyTorchの1epoch平均の学習時間がそれぞれ0.45[sec]，0.28[sec]であることを踏まえると，上記仮設は妥当そうです．
![Core_process_time_comparison_(GPU).png](https://qiita-image-store.s3.amazonaws.com/0/130762/95e2e308-0a8f-041c-1113-4676af43da14.png)


# まとめ
前回立てた「PyTorchの学習時間がChainerよりも早いのは，Loss関数のbakcward計算がCで(ネイティブに)実行されるためである」という仮説は半分正解で半分不正解というような結果となりました．Loss関数のbackward計算の実装方法の差異による学習時間への影響は些細なもののようです．学習時間へ支配的な影響を与えているのは，ネットワークの各層を計算するために要するConvolutionなどの実装方法のようです．分かってしまえば当たり前といえば当たり前な結論ですね．
ただし，今回の実験ではPyTorchの0.1.10を使いましたが，執筆時点の最新版である0.1.12でも実験をしてみたところ，むしろChainerの方が早いという結果になりました．PyTorchはまさしく開発中なんだなという印象です．コードは[github](https://github.com/ynaka81/DeepPoseComparison)にあるので，開発が落ち着いたころに再度実験してみようと思います．