ONNXモデルを内蔵GPUで推論する。

ONNXモデルをグラボが無くても（CPUより）もっと速く推論できないか、ということで内蔵GPUで推論してみました。

# 環境構築

### PCの要件
onnxruntime-directmlというパッケージを使うので、PCは以下の要件を満たす必要があります。
- DirectX 12対応CPU（Intelなら第4世代以降）
- Windows 10, version 1903以降のWindows

詳細は以下
https://onnxruntime.ai/docs/execution-providers/DirectML-ExecutionProvider.html#requirements

### Pythonパッケージ

- Pillow（画像読み込み用）
- numpy（前処理用）
- onnxruntime-directml

インストール後、pip freezeすると以下のようになりました（Python 3.10.9を使用）。
```requirements.txt
coloredlogs==15.0.1
flatbuffers==22.12.6
humanfriendly==10.0
mpmath==1.2.1
numpy==1.24.1
onnxruntime-directml==1.13.1
packaging==22.0
Pillow==9.3.0
protobuf==4.21.12
pyreadline3==3.4.1
sympy==1.11.1

```

requirements.txtからインストールする場合は、以下のようなコマンドを実行します。
```
pip install --no-deps -r requirements.txt
```

### 学習済みモデル

以下からResNet50を拝借しました。
https://github.com/onnx/models/tree/main/vision/classification/resnet#model

### ラベル

以下から拝借しました。
https://github.com/onnx/models/blob/main/vision/classification/synset.txt

### テスト画像

![input.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609103/6d4f524f-38c8-d15d-1c90-fdaeef399686.jpeg)

以下から拝借しました。
https://gahag.net/010175-french-bulldog/
※ 上で表示されている画像は前処理で加工された状態のものです。

# 推論コード

```python
from PIL import Image

import numpy as np
import onnxruntime as ort


def preprocess(pil_img):
    # 画像サイズを224x224に縮小
    # アスペクト比が変わらないよう画像の中心から最大サイズの正方形で抽出してから縮小
    crop_length = min(pil_img.height, pil_img.width)
    crop_left = (pil_img.width - crop_length) // 2
    crop_upper = (pil_img.height - crop_length) // 2
    crop_right = crop_left + crop_length
    crop_lower = crop_upper + crop_length
    pil_img = pil_img.crop((crop_left, crop_upper, crop_right, crop_lower))
    pil_img = pil_img.resize((224, 224))

    # PIL -> numpy
    img = np.asarray(pil_img)

    # 正規化
    mean = [0.485, 0.456, 0.406]
    std = [0.229, 0.224, 0.225]
    img = (img / 255 - mean) / std

    # HWC -> NCHW
    img = np.transpose(img, axes=[2, 0, 1])
    img = np.expand_dims(img, axis=0)

    # double -> float
    img = np.float32(img)

    return img


pil_img = Image.open("gahag-0101751929.jpg")
img = preprocess(pil_img)

providers = ["DmlExecutionProvider"]
ort_sess = ort.InferenceSession("resnet50-v1-7.onnx", providers=providers)
outputs = ort_sess.run(None, {"data": img})

with open("synset.txt", "r") as f:
    labels = [line.rstrip() for line in f]

print(labels[outputs[0][0].argmax()])
```

実行結果は以下の通りで、正しく推論されました。
```
n02108915 French bulldog
```

ちなみに、`providers`で`CPUExecutionProvider`を指定するとCPUで推論することもできます。
```python
(前略)
# providers = ["DmlExecutionProvider"]
providers = ["CPUExecutionProvider"]
ort_sess = ort.InferenceSession("resnet50-v1-7.onnx", providers=providers)
outputs = ort_sess.run(None, {"data": img})
(後略)
```

# 推論時間

推論時間計測用のコードは以下の通りです。
```python
from PIL import Image

import numpy as np
import onnxruntime as ort

import timeit


def preprocess(pil_img):
    # 画像サイズを224x224に縮小
    # アスペクト比が変わらないよう画像の中心から最大サイズの正方形で抽出してから縮小
    crop_length = min(pil_img.height, pil_img.width)
    crop_left = (pil_img.width - crop_length) // 2
    crop_upper = (pil_img.height - crop_length) // 2
    crop_right = crop_left + crop_length
    crop_lower = crop_upper + crop_length
    pil_img = pil_img.crop((crop_left, crop_upper, crop_right, crop_lower))
    pil_img = pil_img.resize((224, 224))

    # PIL -> numpy
    img = np.asarray(pil_img)

    # 正規化
    mean = [0.485, 0.456, 0.406]
    std = [0.229, 0.224, 0.225]
    img = (img / 255 - mean) / std

    # HWC -> NCHW
    img = np.transpose(img, axes=[2, 0, 1])
    img = np.expand_dims(img, axis=0)

    # double -> float
    img = np.float32(img)

    return img


pil_img = Image.open("gahag-0101751929.jpg")
img = preprocess(pil_img)

providers = ["DmlExecutionProvider"]
# providers = ["CPUExecutionProvider"]
ort_sess = ort.InferenceSession("resnet50-v1-7.onnx", providers=providers)

inference_time = timeit.timeit(
    lambda: ort_sess.run(None, {"data": img}), number=1
)
print(f"1回目の推論時間：{inference_time * 1000:.2f} ms")

inference_times = timeit.repeat(
    lambda: ort_sess.run(None, {"data": img}), repeat=100, number=1
)
mean = np.mean(inference_times)
std = np.std(inference_times)
print(f"2回目以降の推論時間：平均{mean * 1000:.2f} ms（標準偏差：{std * 1000:.2f} ms）")
```

以下のCPU（内蔵GPU）で計測しました。
- AMD Ryzen 5 PRO 3500U（AMD Radeon(TM) Vega 8 Graphics）
  - ノートPCなのでバッテリー駆動時と電源接続時の場合も計測
- Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz（Intel(R) UHD Graphics 630）

結果は以下のようになりました。
<table>
  <thead>
    <tr>
      <th colspan="3">計測環境</th>
      <th>1回目の推論時間</th>
      <th>2回目以降の推論時間</th>
    </tr>
  </thead>
  <tr>
    <td rowspan="4">AMD</td>
    <td rowspan="2">バッテリー駆動時</td>
    <td>CPU</td>
    <td>99.67 ms</td>
    <td>平均108.84 ms（標準偏差：21.70 ms）</td>
  </tr>
  <tr>
    <td>内蔵GPU</td>
    <td>3032.95 ms</td>
    <td>平均105.70 ms（標準偏差：0.54 ms）</td>
  </tr>
  <tr>
    <td rowspan="2">電源接続時</td>
    <td>CPU</td>
    <td>84.48 ms</td>
    <td>平均62.72 ms（標準偏差：14.13 ms）</td>
  </tr>
  <tr>
    <td>内蔵GPU</td>
    <td>1667.75 ms</td>
    <td>平均26.08 ms（標準偏差：2.14 ms）</td>
  </tr>
  <tr>
    <td rowspan="2" colspan="2">Intel</td>
    <td>CPU</td>
    <td>17.39 ms</td>
    <td>平均16.86 ms（標準偏差：5.78 ms）</td>
  </tr>
  <tr>
    <td>内蔵GPU</td>
    <td>2361.29 ms</td>
    <td>平均89.61 ms（標準偏差：1.89 ms）</td>
  </tr>
</table>

※ Intelは内蔵GPUを使うと反って遅くなってしまいましたが、世代が古いからなのかもしれません。最新世代をお持ちの方はぜひ試してみてください。