AIエッジコンテスト(実装コンテスト)のチュートリアル【5: PyTorchをハックしてテストベンチとパラメータを読出す】

いよいよFPGAにハードウェアアクセラレータを作りましょう！‥という準備編です。ハードウェア設計は面倒ですねぇ。。この連載を通じて、なぜハードウェア設計は時間がかかるのか、そしてなぜハードウェア技術者（特にAI関係）が非常に少なく、かつ、なり手が少ないのかを実感できると思います（笑

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/589480/d965a154-1ea0-4817-dbe1-1488fe7b93f7.png)


### 今回やること
前回の図を使い回しで恐縮ですが、Xilinx社のFPGAに所望の回路を設計するには

+ 高速化したい処理を行う回路（とデータを保持するオンチップメモリ）
+ ホストPSと通信するAXI4 Stream通信回路
+ 検証用のテストベンチ

が必要でした。今回はPyTorchを使って、学習済みのモデルからテストベンチ（入出力値）と重みパラメータを生成してハードウェア設計の準備を行います。その前に、今回設計対象となる畳み込み演算について解説します。

### 畳み込み演算とは
Convolutional Operationともいい、今回のコンテストの題材になっている物体検出用のニューラルネットワーク(CNN: Convolutional Neural Networkともいう)で約90%以上を占めているという演算です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/589480/d3c87a49-fb84-01bf-98b2-9506a12b6c12.png)

上の図を見てください。CNNのある畳み込み層の計算を示しています。左から入力特徴マップ、重み、特徴マップと重みを畳み込んだ中間値u0〜uc、それらの総和を行いバイアス値を加算した結果に活性化関数を適用し、出力特徴マップに保存します。特徴マップはc個のチャネルで構成されており、そのサイズはn×nとしましょう（上の図では4×4）。畳み込み演算では出力チャネル毎に(入力チャネル数×サイズ)の重みを畳み込むので、重みのサイズは(出力チャネル数×入力チャネル数×入力チャネルサイズ)となります。また、各中間値uiは重みの各チャネル毎のサイズ3×3(であることが多い)ので、一つの出力値を計算するのに(入力チャネル数×入力チャネルサイズ×3×3)回の積和演算を実行する必要があります。通常、活性化関数は正の値はそのままであり、負の値を０とするReLU関数を用いることが多いです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/589480/2fc84ebd-7426-a9cc-83f8-3bede937ff88.png)

上の図のように、次の出力値を求めるには、重みはそのままで入力だけをずらします。従って、出力特徴マップ・出力特徴マップそれぞれのサイズをc×n×n、重みのサイズを3×3とすると、その積和演算回数は3×3×c×n×n×cとなります。CNNの層数は10〜100ぐらい、c=64〜512ぐらいですから、その計算回数はとても多いことがわかると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/589480/5717cf4c-037d-ab6e-e48d-9615118bdcef.png)

通常、CNNでは入力と出力マップのサイズが不変になるように（そうでないと、層を深くすると徐々に消失してしまうので）、上図に示すように、入力特徴マップの周辺に０ビットを詰めるパディングという作業を行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/589480/4c5dd14d-f6a3-03aa-b7bc-8d8b3ed84c7f.png)

また、上図の下に示すように、重みをずらす量をストライドといい、この値を増やすことで逆に出力特徴マップのサイズを縮小することも行います。通常はパディングとストライドの量をうまく組み合わせてCNNのモデルを適切に調整してデータの複雑度に合うようにし、過学習や未学習を抑制しつつ、求められる計算速度やハードウェアサイズを満たすようにします。大変です、これだけでも。。（なので、自動で求めるNeural Architecture Search (NAS)が重宝されるのです）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/589480/79817d70-d2f1-f9a1-591f-b98aac30438b.png)

一般的な畳み込み演算は上図のような立方体の図を使って表すことが多いです。特に論文では断りもなく使われるので、慣れておきましょう。


### PyTorchをハックしてCNNモデルにアクセスする
Ubuntu18.04LTSをインストールしたホストPC上で（Ultra96ボードは遅いので奨励しません！！）、PyTorchをインストールします。インストール方法はPyTorch公式のページを見てください。GPUを持っていない場合でもOKです。

+ torch 1.4.0
+ torchvision 0.5.0
+ opencv
+ matplotlib

上記をpip, またはconda経由でインストールします。

コンテストサンプルのリポジトリをクローンしてください。

```
$ git clone https://github.com/HirokiNakahara/FPGA_AI_Edge_Contest_2019
```

テストベンチ用のディレクトリに移動します。

```
$ cd TestBenchGen
```

学習データを置いているディレクトリへのシンボリックリンク（ショートカット）を貼ります。

```
$ ln -s (学習データを置いているディレクトリへのパス) data
```

次に、[学習済みモデルをダウンロード](https://drive.google.com/file/d/1xUznZs1YPh4XBPri9zcpfk2WTSlDhcVK/view?usp=sharing)するか、第２回のチュートリアルで学習したモデルを用意して、カレントディレクトリにモデルを置くディレクトリを作成し、コピーします。

```
$ mkdir yolov2_alex_1
$ cp (モデルを置いている場所) ./yolov2_alex_1
```

※この例では ./yolov2_alex_1/yolov2_epoch_40.pth としました。

準備ができたのでプロファイルを取ってモデルのパラメータにアクセスしてみましょう。以下のコマンドを実行してください。

```
$ python gen_testbench.py --arch alex --dataset test --output_dir yolov2_alex_1 --model_name yolov2_epoch_40 --bs 1
```

モデルを置いている場所(--output_dir)、モデル名(model_name, 拡張子.pthは除く)は適時読み替えてください。実行すると、プロファイル結果が端末に表示され、以下のファイルが生成されるはずです。

第１層畳み込み演算のテストベンチ

+ testbench_input.txt
+ testbench_output.txt

第１層畳み込み演算の重みとバイアス値

+ weight_l0.txt
+ bias_l0.txt

### プロファイルの方法
様々な方法がありますが、簡単な方法はPyTorchのライブラリを使う方法です。`yolov2_test.py`を見てください。

```python:yolov2_test.py
        with torch.autograd.profiler.profile(use_cuda=False) as prof:
            tmp = x
            tmp = self.trunk(tmp)
            tmp = self.conv3(tmp)
            out = self.conv4(tmp)
        print(prof)
        print(prof.key_averages().table(sort_by="self_cpu_time_total"))
```

`torch.autograd.profiler.profile`を使ってprofオブジェクトにプロファイル結果を記録します。あとは表示するだけですね。`self_cpu_time_total`に関数毎に集計しますが、これを見ると

```
mkldnn_convolution           82.19%
```

となっていますから、やはり畳み込み演算を高速化するのがよさそうです。

### 学習済みモデルから重みを読み出す

`yolov2_test.py`をさらに見ていきましょう。

```python:yolov2_test.py
        # ------------------------------------------------
        # Access parameters, 
        #  and generate weight and bias for the 1st layer
        # ------------------------------------------------
        for n, p in part_module.named_parameters():
            print("layer", n)
            print("shape", p.shape)

            if n == '0.weight':
                param = p.detach().numpy()
                print('weight',param)
                fname = 'weight_l0.txt'
                print('Weight Paramter File -> %s' % fname)
                np.savetxt(fname, param.reshape(-1,), fmt="%.4f", delimiter="")

            if n == '0.bias':
                param = p.detach().numpy()
                print('bias',param)
                fname = 'bias_l0.txt'
                print('Bias Paramter File -> %s' % fname)
                np.savetxt(fname, param.reshape(-1,), fmt="%.4f", delimiter="")
```

PyTorchでパラメータにアクセスするには様々な方法がありますが、簡単なのは順序つき辞書にアクセスして、名前と値を読み出す方法です。今回は第一層の畳み込みをハードウェア化してみましょう。

該当する層のパラメータを探す

```python:
            if n == '0.weight':
```

numpyに変換

```python:
                param = p.detach().numpy()
```

テキストファイルに出力

```python:
                fname = 'weight_l0.txt'
                print('Weight Paramter File -> %s' % fname)
                np.savetxt(fname, param.reshape(-1,), fmt="%.4f", delimiter="")
```

すると学習済みパラメータを再利用できます。TensorflowでもMXNETでも同様にnumpyへの変換ができるので、同じ原理で学習済みモデルをC++等の他の言語で再利用できますね。

### 特定の層にアクセスして入出力テストベンチを作成する

`yolov2_test.py`を更に追ってみましょう。

```python:yolov2_test.py
        # ------------------------------------------------
        # Test(Inference) step-by-step manner
        #  and generate testbenches
        # ------------------------------------------------
        y = x
        for layer_idx, module in enumerate(part_module):
            print("layer idx",layer_idx)

            # Printout layer information (to be uncommented)
            #print("module",module)
            #print(type(module))
            #print(module.__dict__)
            #print(module.__dict__['_modules'])

            # To access a specified layer (just example)
            #if isinstance( module, torch.nn.modules.conv.Conv2d):

            # Generate Testbench
            if layer_idx == 0:
                print("conv2d in the 1st layer!!")

                bench_input = y
                bench_input = bench_input.numpy()
                print(bench_input.shape)
                print("bench_input",bench_input)
                bench_input = bench_input.reshape(-1,)
                fname = 'testbench_input.txt'
                print('Test Bench Input File -> %s' % fname)
                np.savetxt(fname, bench_input, fmt="%.5f", delimiter=",")

                print('input.dimension',y.shape)
                y = module(y) # (1,3,416,416)
                print('output.dimension',y.shape)

                print('y', y.shape) # (1,64,102,102)

                bench_output = y
                bench_output = bench_output.detach().numpy()
                bench_output = bench_output.reshape(-1,)

                fname = 'testbench_output.txt'
                print('Test Bench Output File -> %s' % fname)
                np.savetxt(fname, bench_output, fmt="%.5f", delimiter=",")

            else:
                y = module(y)
```

PyTorchは層の関数とパラメータを分離して管理しているので、`enumerate()`を使って層毎にアクセスすることが可能です。今回は第１層のみハードウェア化するので、該当するインデックスを探し

```python:
            if layer_idx == 0:
                print("conv2d in the 1st layer!!")
```

入力値をnumpyに変換してファイル出力をします。

```python:
                bench_input = y
                bench_input = bench_input.numpy()
                bench_input = bench_input.reshape(-1,)
                fname = 'testbench_input.txt'
                print('Test Bench Input File -> %s' % fname)
                np.savetxt(fname, bench_input, fmt="%.5f", delimiter=",")
```

層にアクセスして出力を得ましょう。

```python:
                y = module(y) # (1,3,416,416)
```

出力値もファイルに出力します。

```python:
                bench_output = y
                bench_output = bench_output.detach().numpy()
                bench_output = bench_output.reshape(-1,)

                fname = 'testbench_output.txt'
                print('Test Bench Output File -> %s' % fname)
                np.savetxt(fname, bench_output, fmt="%.5f", delimiter=",")
```

途中で`print()`を使ってデバッグも可能ですね。次回以降の設計の時にC++との値との比較が可能です。

### 次回は
ふー、やっとハードウェア設計の準備ができました。次はいよいよ高位合成を使ってハードウェアを設計してみましょう。