はじめに
私たちの開発では、技術探索やアルゴリズム検証を Python で行い、その後のプロダクトコードを Rust で実装することがあります。
この流れ自体は自然です。
Python は numpy や OpenCV、scikit-learn など探索に必要な道具が揃っており、試行錯誤のスピードが非常に速いです。
一方で、Python で開発されたアプリケーションをそのままプロダクトとして運用する場合、
- 実行環境にPythonがインストールされている必要がある。
- あるいは、PyInstallerで作成された巨大なexeファイルを配布する必要がある
など運用しにくい面があります。
そこで、プロダクトとして長く運用するコードでは、Rust の型安全性や性能、依存管理のしやすさが好ましくなります。
が、ここで一つ問題があります。
Python には scikit-learn のような「定番の型」がある一方で、Rust ではそのような共通の型がまだ広く浸透しているわけではありません。
そのため、Python なら定番パッケージで完結する処理も、用途に応じてクレートを選び、組み合わせる必要があります。
また、Python で提供されるパッケージは内部が C++で実装されていることも多く、性能面でも最適化されています。
今回はPythonで検討した内容をプロダクトコードとしてRustに落とし込むにあたって、活用しているクレート、速度面で意識している事を紹介します。
実際の所、Rust ではどうするの?
Python では、次のような流れが自然に組めます。
- numpy で配列処理
- OpenCV で前処理
- scikit-learn で学習・評価
- PyTorchで深層学習
パッケージ類がかなり充実しています。
実はRust でも同様の処理を実現するためのクレートが提供されています。
- 配列処理:ndarray
- 線形代数:nalgebra
- 推論:ort(ONNX Runtime)
- LightGBM:lightgbm3
- SVM:light-svm など
部分的に自前でコードを書かないといけない部分もありますが、想定以上にクレートが整備されており、実務でも十分活用可能です。
各クレートの紹介
ort(ONNX Runtime)
Python 側で学習・変換した深層学習モデルを Rust で推論する際の有力な選択肢です。
深層学習のネットワークを更新する事自体はできませんが、Pythonで実装した推論処理を比較的容易に実現することが可能です。
lightgbm3
Rust から LightGBM を利用するためのクレートです。
探索段階で LightGBM が有効だと分かっているなら、プロダクト側でも同じアルゴリズムを実現することが可能です。
light-svm
SVM もRustで簡単に使う事が可能です。
scikit-learn のような統一的な体験はありませんが、問題設定が明確であれば十分実用的です。
機械学習のプロダクト実装では、学習器そのものよりも前処理や推論フローに工数がかかることが多く、既存クレートを活用できるメリットは大きいです。
nalgebra
線形代数を扱うためのクレートです。
ベクトルや行列を数式に近い感覚で扱えるため、アルゴリズム実装の見通しが良くなります。
配列処理としての ndarray と、線形代数としての nalgebra を使い分けるのがポイントです。
ndarray
Rust における numpy 的な存在で、多次元配列の中心的なクレートです。
前処理、特徴量、モデル入出力など、機械学習系の処理は最終的に配列操作に集約されることが多く、このクレートが基盤になります。
Rustコードの高速化で意識すること
余談になりますが、機械学習処理では推論時間も重要なファクターとなってきます。
そして、Rust で数値計算や画像処理のコードを書く際、処理速度は「コードの書き方」だけでなく、ビルド設定や依存関係の選択によって大きく変わります。
インストールオプション・featureが性能に影響する
Rust では、クレートの機能や最適化は cargo.toml の設定によって明示的に選択します。
たとえば ndarray を用いた行列演算で性能が重要な場合、BLAS を利用することで高速化が可能です(特に行列積が支配的な処理で効果が出ます)。
ただしこれは自動では有効にならず、feature を明示的に設定する必要があります。
[dependencies]
ndarray = { version = "0.15", features = ["blas"] }
blas-src = { version = "0.8", features = ["openblas"] }
このように、「どのバックエンドを使うか」「どの最適化を有効にするか」を自分で選ぶ必要があります。
これは一見手間ですが、裏を返せばどこをどれだけ高速化するかを制御できるということでもあります。
debugビルドとreleaseビルドの違い
Rust ではビルドモードによって性能が大きく変わります。
- debug ビルド:最適化なし(実行速度は遅い)
- release ビルド:最適化あり(高速に実行される)
特に数値計算ではこの差が非常に大きく、debug ビルドでの実行結果だけを見て性能を判断すると誤った評価になります。(そして、一回はやらかします)
そのため、性能を確認する際は必ず release ビルドで実行する必要があります。
cargo run --release
実務での進め方
実際には、次のようなステップで進めるのが現実的です。
- まずはシンプルに実装する
- release ビルドで性能を測定する
- プロファイルしてボトルネックを特定する
- 必要な箇所だけ feature や依存関係(BLAS など)を調整する
最初から最適化を詰め込むのではなく、必要な部分にだけ適用することで、開発速度と性能のバランスを取りやすくなります。
まとめ
scikit-learn のような「定番の型」を提供するパッケージが充実している Python と同様に、Rust でも用途ごとに各種クレートが整備されている事を紹介しました。
さらに Rust では、コードだけでなく cargo.toml の設定やビルドモードも含めて性能設計を行う必要がありますが、その分、最適化のコントロール性が高いという特徴があります。
Python で探索し、Rust で実装するという流れの中で、こうした違いを理解しておくことで、より現実的なプロダクト設計が可能になります。
Rust で同様の課題に取り組んでいる方の参考になれば幸いです。