[Rust] PyTorchで作成したONNXモデルをBurnで変換して使う [Deep Learning]

2023.10.19

Introduction

burnはRust用Deep Learningフレームワークです。
現在アクティブに開発が進められているようで、
今後が期待できるプロダクトです。

公開されているMNISTデモはこちら

今回はこのburnを用いて、ONNX形式の既存モデルを
burn用モデルに変換して使ってみます。

Burn?

burnは2021年にリリースされた新しめの深層学習フレームワークです。
少し使ってみた感じだと、PyTorchに近い感じです。

burnの特徴は、以下のとおりです。

Tensor

Tensor(テンソル)は、深層学習フレームワークを使う際の
基本的なデータ構造であり、
多次元の数値データを表現するために使用します。
burnでも例によってTensor構造体を使います。
このあたりも既存のフレームワークを使い慣れている人なら
馴染みやすいかと思います。

バックエンド

burnではだいたいの実装がBackendトレイトに基づいており、
バックエンドを切り替えることによりいろいろな実装でTensorの演算を使用できます。
現在は下記のようにTorch、ndarray、wgpuの3種類のバックエンドに対応。

  • Torch : CPU・GPUともにサポート
  • Ndarray : CPUのみ。no_stdもサポート
  • WebGPU : GPU専用。WASMも?

Ndarrayはno_std対応しているので、使用環境の幅が広がりますね。

サンプル付属

ここに、いろいろなサンプルがありますので、すぐにビルドして動作確認できます。

Datasetやimport

burnは機械学習データパイプラインの作成プロセスを
効率化するためのいろいろなデータセット実装や変換ロジック、
データソースを提供します。

また、burn-importは、ONNX形式のモデルを変換し、Burn用にRustコードを生成します。
(今回試すやつ)

その他、burnの情報についてはこことかを参考にしてください。

Environment

  • MacBook Pro (13-inch, M1, 2020)
  • OS : MacOS 13.5.2
  • Python : 3.8.8
  • Rust : 1.74.0

Try

では、PyTorchで適当なモデルを作成してONNXでexportし、
そのモデルをburnで変換して使ってみます。

PyTorchで適当なモデルを作成

まずはベースとなるモデルをPyTorchで作成。
↓みたいな適当なCSV(train_data)を学習させ、
Weightを渡したらHeightを推論するモデルを作成します。

Weight,Height
60.3,164.1
59.0,168.5
44.7,172.1
47.9,166.4
57.1,164.7
38.9,163.5
・
・
・

必要なライブラリをインストール。

pip3 install torch numpy pandas

PyTorchで学習&推論するサンプルを作成します。

import torch
import torch.nn as nn
import pandas as pd
import torch.onnx

# データセットの読み込み
df = pd.read_csv('train_data.csv')

# 入力と出力を分割
weights = df['Weight'].values
heights = df['Height'].values

# データの前処理
weights = (weights - weights.mean()) / weights.std()  # 標準化

# PyTorchのテンソルに変換
weights = torch.tensor(weights, dtype=torch.float).view(-1, 1)
heights = torch.tensor(heights, dtype=torch.float).view(-1, 1)

# モデルの定義
class RegressionModel(nn.Module):
    def __init__(self):
        super(RegressionModel, self).__init__()
        self.linear = nn.Linear(1, 1)
    def forward(self, x):
        return self.linear(x)

model = RegressionModel()

# 損失関数と最適化関数の定義
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

# 学習
num_epochs = 1000
for epoch in range(num_epochs):
    # forward
    outputs = model(weights)
    loss = criterion(outputs, heights)

    # backwardとパラメータ更新
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    # 途中結果の表示
    if (epoch+1) % 100 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')

# モデルの推論
weights = [60.,70.,80.]
test_weights = torch.tensor(weights, dtype=torch.float32).view(-1, 1) 

predicted_height = model(test_weights)
for i in range(len(test_weights)):
    print(f'Test Sample {i + 1}:  Weights = {weights[i]} , Predicted Height={predicted_height[i][0]}')

実行してみます。
結果はともかく作成したモデルで推論が動いてます。

% python3 main.py
Epoch [100/1000], Loss: 526.2801
Epoch [200/1000], Loss: 32.2470
Epoch [300/1000], Loss: 23.5581
Epoch [400/1000], Loss: 23.4052
Epoch [500/1000], Loss: 23.4026
Epoch [600/1000], Loss: 23.4025
Epoch [700/1000], Loss: 23.4025
Epoch [800/1000], Loss: 23.4025
Epoch [900/1000], Loss: 23.4025
Epoch [1000/1000], Loss: 23.4025
Test Sample 1:  Weights = 60.0 , Predicted Height=185.85348510742188
Test Sample 2:  Weights = 70.0 , Predicted Height=189.27638244628906
Test Sample 3:  Weights = 80.0 , Predicted Height=192.69927978515625

ONNXで出力

ONNXはOpen Neural Network eXchangeの略で、
機械学習モデルを表現するためのフォーマットです。
TensorflowやPyTorchなど主要なフレームワークはONNX変換できるので、
burnでそのままimportできます。

さきほどのPyTorchのプログラムを↓のようにしてONNXで出力します。

# ダミーのinputデータを生成
dummy_input = torch.zeros(1, 1)  # ダミーのテンソル

# モデルをONNX形式にエクスポート
torch.onnx.export(model,                     # モデル
                  dummy_input,               # ダミー入力データ
                  'model.onnx',              # 出力ファイル名
                  export_params=True,        # パラメータを含めるかどうか
                  opset_version=9,           # ONNXのバージョン
                  do_constant_folding=True,  # 定数折りたたみを行うかどうか
                  input_names=['input'],     # 入力の名前
                  output_names=['output'])   # 出力の名前

Rust

Cargoでプロジェクトを作成し、さきほどのonnxファイルをコピーしておきます。

% cargo new burn_example && cd burn_example
% mkdir ./src/ptmodel
% cp /<先程のonnxファイルパス>/model.onnx ./src/ptmodel/

Cargo.tomlに依存ライブラリを記述。
build-dependenciesにburn-importを追加するのを忘れずに。

[dependencies]
burn = { version = "0.9.0", features = ["ndarray", "std", "wgpu", "tch", "train"] }
serde = "1.0.189"

[build-dependencies]
burn-import = "0.9.0"

ビルド時にonnxからrsファイルを生成するため、
burn_exampleディレクトリにbuild.rsを作成します。

use burn_import::onnx::{ModelGen, ONNXGraph};

fn main() {
    ModelGen::new()
        .input("src/ptmodel/model.onnx")
        .out_dir("ptmodel/")
        .run_from_script();
}

ModelGenを使うことで、指定したパスにある
onnxファイルを任意のディレクトリに出力できます。
上記のbuild.rsで実際にビルドすると、
burn_example/target/debug/build/burn_example-xxx/out/ptmodel
に出力されます。

ptmodel/mod.rsを作成し、
ModelGenで出力したmodel.rsファイルをincludeマクロで読み込みます。

pub mod model {
    include!(concat!(env!("OUT_DIR"), "/ptmodel/model.rs"));
}

main.rsではbuild.rsで生成されるrsファイルを使ってコードを記述します。

mod ptmodel;

use ptmodel::model::*;
use burn::tensor::Tensor;
use burn::backend::NdArrayBackend;

type Backend = NdArrayBackend;

fn main() {
    // Create Model
    let model: Model<Backend> = Model::default();
    // Create a new input tensor
    let input = Tensor::<NdArrayBackend<f32>, 2>::from_data([[60.],[70.],[80.]]);
    // Run the model
    let output = model.forward(input);
    // Print the output
    println!("{:?}", output);
}

実行してみます。推論できてますね。

% cargo run
・
・
・
Tensor { primitive: NdArrayTensor { 
array: [[185.85349],[189.27638], [192.69928]], 
shape=[3, 1], strides=[1, 1], layout=CFcf (0xf), dynamic ndim=2 } }

ちなみに、main.rsで↓のようにすれば生成されたファイルの中身がみれます。

println!("{}",include_str!(concat!(env!("OUT_DIR"), "/ptmodel/model.rs")));

Summary

今回はburnを使ってonnx形式のモデルを変換して使ってみました。
簡単にモデル変換と推論ができました。

なお、ここにburnのドキュメントがあるのですが、
目次に「8.3. WebAssembly」「8.4. No-Std」などおもしろそうなのがあるので、
(追加されたら)確認してみようかと思います。  

References