ねらい
PyTorchで作成したモデルをONNX形式に変換する方法を実際に手を動かして理解する。
対象
- PyTorchでモデルを訓練したことがある人
- モデルをデプロイしたい人
- 「ONNXに変換したいけど、やり方がわからない」人
ゴール
- torch.onnx.export() を使いこなせるようになる
- 動的入力サイズへの対応ができる
- 変換後のモデルを検証できる
TL;DR
-
torch.onnx.export()で変換する - ダミー入力が必要(トレーシングのため)
-
dynamic_axesで可変サイズに対応 -
onnx.checker.check_model()で検証
環境準備
まずは必要なライブラリをインストールしよう。
pip install torch onnx onnxruntime
バージョン確認:
import torch
import onnx
import onnxruntime as ort
print(f"PyTorch: {torch.__version__}")
print(f"ONNX: {onnx.__version__}")
print(f"ONNX Runtime: {ort.__version__}")
執筆時点での推奨バージョン:
- PyTorch 2.0以上
- ONNX 1.14以上
- ONNX Runtime 1.15以上
最もシンプルな変換
まずは基本中の基本から。シンプルなMLPを変換してみよう。
モデルの定義
import torch
import torch.nn as nn
class SimpleMLP(nn.Module):
def __init__(self, input_dim=784, hidden_dim=256, output_dim=10):
super().__init__()
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
return x
# モデルのインスタンス化
model = SimpleMLP()
model.eval() # 推論モードに設定(重要!)
ONNX変換
# ダミー入力を作成(トレーシングに必要)
dummy_input = torch.randn(1, 784)
# ONNXにエクスポート
torch.onnx.export(
model, # 変換するモデル
dummy_input, # ダミー入力
"simple_mlp.onnx", # 出力ファイル名
export_params=True, # 重みも一緒に保存
opset_version=11, # opset version(11以上推奨)
input_names=["input"], # 入力の名前
output_names=["output"] # 出力の名前
)
print("変換完了!")
たったこれだけ。simple_mlp.onnx というファイルが生成される。
変換結果の検証
import onnx
# モデルを読み込み
onnx_model = onnx.load("simple_mlp.onnx")
# モデルの検証(不正な場合は例外が発生)
onnx.checker.check_model(onnx_model)
print("モデルは正常です")
# グラフの情報を表示
print(f"グラフ名: {onnx_model.graph.name}")
print(f"ノード数: {len(onnx_model.graph.node)}")
torch.onnx.export() のパラメータ詳解
公式ドキュメントから重要な部分を見てみよう。
Exporting a model in PyTorch works via tracing or scripting. This tutorial will use as an example a model exported by tracing. To export a model, we call the torch.onnx.export() function. This will execute the model, recording a trace of what operators are used to compute the outputs.
(PyTorchでのモデルのエクスポートは、トレーシングまたはスクリプティングを通じて動作する。このチュートリアルでは、トレーシングによってエクスポートされたモデルを例として使用する。モデルをエクスポートするには、torch.onnx.export()関数を呼び出す。これにより、モデルが実行され、出力を計算するために使用されるオペレータのトレースが記録される。)
主要なパラメータ
torch.onnx.export(
model, # nn.Module または ScriptModule
args, # ダミー入力(タプルまたはテンソル)
f, # 出力ファイルパス
export_params=True, # 学習済みパラメータを含めるか
verbose=False, # 詳細なログを出力するか
training=TrainingMode.EVAL, # 訓練モード or 推論モード
input_names=None, # 入力テンソルの名前
output_names=None, # 出力テンソルの名前
opset_version=None, # ONNXのopset version
dynamic_axes=None, # 動的な次元の指定
do_constant_folding=True, # 定数畳み込み最適化
)
特に重要なパラメータ
export_params
True にすると学習済みの重みがモデルに含まれる。本番用なら True 一択。
opset_version
前回説明したopset version。明示的に指定しよう。
# 推奨: 11以上を明示的に指定
opset_version=11
do_constant_folding
定数の事前計算を行う最適化。通常は True でOK。
# 例: a = 2 * 3 のような定数計算を、変換時に a = 6 に置き換える
do_constant_folding=True
動的入力サイズへの対応
実運用では、バッチサイズや入力サイズが変わることが多い。固定サイズだと不便だ。
Note that the input size will be fixed in the exported ONNX graph for all the input's dimensions, unless specified as a dynamic axes.
(入力サイズは、動的軸として指定されない限り、エクスポートされたONNXグラフ内のすべての入力次元で固定されることに注意。)
dynamic_axes の使い方
# バッチサイズを可変にする
torch.onnx.export(
model,
dummy_input,
"model_dynamic.onnx",
opset_version=11,
input_names=["input"],
output_names=["output"],
dynamic_axes={
"input": {0: "batch_size"}, # 0番目の次元を可変に
"output": {0: "batch_size"} # 出力も同様
}
)
複数の可変次元
画像処理モデルなら、高さと幅も可変にしたいことがある。
# 画像モデル用(バッチ、高さ、幅を可変に)
# 入力形状: [batch, channels, height, width]
dynamic_axes={
"input": {
0: "batch_size",
2: "height",
3: "width"
},
"output": {
0: "batch_size"
}
}
シーケンス長を可変にする(NLP)
# テキストモデル用
# 入力形状: [batch, sequence_length]
dynamic_axes={
"input_ids": {
0: "batch_size",
1: "sequence_length"
},
"attention_mask": {
0: "batch_size",
1: "sequence_length"
},
"output": {
0: "batch_size"
}
}
画像分類モデルの変換例
実践的な例として、ResNet18を変換してみよう。
import torch
import torchvision.models as models
# 学習済みResNet18を読み込み
model = models.resnet18(pretrained=True)
model.eval()
# ダミー入力(バッチサイズ1、3チャンネル、224x224)
dummy_input = torch.randn(1, 3, 224, 224)
# ONNXにエクスポート
torch.onnx.export(
model,
dummy_input,
"resnet18.onnx",
export_params=True,
opset_version=11,
do_constant_folding=True,
input_names=["image"],
output_names=["class_logits"],
dynamic_axes={
"image": {0: "batch_size"},
"class_logits": {0: "batch_size"}
}
)
print("ResNet18をONNXに変換しました")
変換結果の確認
import onnx
from onnx import shape_inference
# モデルを読み込んで検証
model = onnx.load("resnet18.onnx")
onnx.checker.check_model(model)
# 形状推論を実行(デバッグに便利)
model_with_shapes = shape_inference.infer_shapes(model)
# 入出力の情報を表示
for input in model.graph.input:
print(f"入力: {input.name}")
for output in model.graph.output:
print(f"出力: {output.name}")
ONNX Runtimeで推論してみる
変換したモデルが正しく動くか確認しよう。
import numpy as np
import onnxruntime as ort
# ONNX Runtimeセッションを作成
session = ort.InferenceSession("resnet18.onnx")
# 入力情報を確認
input_name = session.get_inputs()[0].name
print(f"入力名: {input_name}")
# ダミーデータで推論
dummy_data = np.random.randn(1, 3, 224, 224).astype(np.float32)
outputs = session.run(None, {input_name: dummy_data})
print(f"出力形状: {outputs[0].shape}")
print(f"出力サンプル: {outputs[0][0, :5]}") # 最初の5クラスの値
PyTorchの出力と比較
変換が正しく行われたか、数値的に確認する。
import torch
import numpy as np
import onnxruntime as ort
import torchvision.models as models
# PyTorchモデル
torch_model = models.resnet18(pretrained=True)
torch_model.eval()
# ONNXモデル
ort_session = ort.InferenceSession("resnet18.onnx")
# 同じ入力データを作成
np_input = np.random.randn(1, 3, 224, 224).astype(np.float32)
torch_input = torch.from_numpy(np_input)
# PyTorchで推論
with torch.no_grad():
torch_output = torch_model(torch_input).numpy()
# ONNXで推論
ort_output = ort_session.run(None, {"image": np_input})[0]
# 差分を確認
diff = np.abs(torch_output - ort_output)
print(f"最大差: {diff.max()}")
print(f"平均差: {diff.mean()}")
# 許容誤差内か確認
if np.allclose(torch_output, ort_output, rtol=1e-5, atol=1e-6):
print("変換成功!出力が一致しています")
else:
print("警告: 出力に差異があります")
通常、差異は非常に小さい(1e-6程度)はずだ。
よくあるエラーと対処法
エラー1: TracingWarning
TracingWarning: Converting a tensor to a Python boolean might cause
the trace to be incorrect.
原因: トレース時に、テンソルの値に依存する分岐がある。
# NG: 値に依存する分岐
def forward(self, x):
if x.sum() > 0: # テンソルの値で分岐
return x * 2
else:
return x
対処:
- 条件分岐を避ける
-
torch.where()を使う -
torch.jit.scriptを使う
# OK: torch.whereを使う
def forward(self, x):
return torch.where(x.sum() > 0, x * 2, x)
エラー2: Unsupported operator
RuntimeError: Exporting the operator 'aten::xxx' to ONNX opset version 11
is not supported.
原因: そのオペレータがONNXでサポートされていない。
対処:
- opset versionを上げてみる(サポートされていることがある)
- サポートされている演算で書き換える
- カスタムオペレータを登録する(上級者向け)
エラー3: Dynamic control flow
RuntimeError: Unsupported: ONNX export of operator ... with dynamic inputs
原因: 入力サイズに依存するループや分岐がある。
対処:
-
torch.onnx.exportの代わりにtorch.onnx.dynamo_exportを試す(PyTorch 2.0以降) - モデル構造を見直す
エラー4: eval()を忘れた
症状: 推論結果がおかしい、バッチノーマライゼーションが効いていない。
原因: model.eval() を呼び忘れている。
対処: エクスポート前に必ず model.eval() を呼ぶ。
model.eval() # これ忘れがち!
torch.onnx.export(model, ...)
実践Tips
1. verboseオプションを活用
問題が起きたら、まず詳細ログを見る。
torch.onnx.export(
model, dummy_input, "model.onnx",
verbose=True # 詳細なログを出力
)
2. 段階的に確認
複雑なモデルは、サブモジュールごとに変換を試す。
# エンコーダだけ先に変換してみる
torch.onnx.export(model.encoder, dummy_input, "encoder.onnx", ...)
3. Netronで可視化
変換後は必ずNetronで構造を確認する習慣をつけよう。
# Netron(https://netron.app/)にドラッグ&ドロップ
4. 入出力の名前は意味のあるものに
デバッグ時に役立つ。
# NG
input_names=["input_0"]
# OK
input_names=["image_rgb"]
まとめ
-
torch.onnx.export()で変換する - ダミー入力を忘れずに用意
-
model.eval()を呼んでから変換 - opset_version は明示的に指定(11以上推奨)
- dynamic_axes で可変サイズに対応
- 変換後は
onnx.checker.check_model()で検証 - PyTorchとONNXの出力を比較して確認
次回予告
第4回では、ONNX Runtimeを使った高速推論の実装を解説する。CPU/GPUの使い分け、IOBindingによる最適化、実際のベンチマーク結果も紹介する予定だ。
参考文献
- PyTorch ONNX Tutorial: https://docs.pytorch.org/tutorials/advanced/super_resolution_with_onnxruntime.html
- torch.onnx Documentation: https://docs.pytorch.org/docs/stable/onnx.html
- ONNX Python API: https://onnx.ai/onnx/api/