m2cgen: 機械学習モデルの多言語トランスパイル

近年のAI(人工知能)の発展は目覚ましく、様々な分野でその技術が活用されています。機械学習モデルの構築は、その中心的な役割を担っており、Pythonはその開発言語として広く利用されています。scikit-learnなどのライブラリを用いることで、比較的容易に学習済みモデルを作成することが可能です。

しかし、学習済みモデルを実際に組み込みシステムWebフロントエンド環境、あるいはPython以外のバックエンド言語RubyGo言語など)でデプロイしようとすると、様々な課題に直面します。

例えば、組み込みシステムでは、計算資源やメモリが限られているため、Pythonで学習させたモデルをそのまま動作させることは困難です。また、Webフロントエンドでは、JavaScriptでモデルを実行する必要があるため、Pythonで学習させたモデルをJavaScriptに変換する必要があります。さらに、Python以外のバックエンド言語でモデルをデプロイする場合、言語間の互換性の問題や、機械学習モデルのアルゴリズムに関する深い理解が必要となる場合があります。

これらの課題を解決するために、機械学習モデルを様々な言語に変換するツールが開発されています。本記事では、その中でも「m2cgen」というツールについて解説します。m2cgenを利用することで、Pythonで学習させたモデルを様々な言語のコードに変換することができます。

m2cgen とは?

m2cgenは、学習済みの機械学習モデルを様々なプログラミング言語のコードに変換するためのツールです。このパッケージを利用することで、Pythonで学習させたモデルをC言語、Java、JavaScript、Goなど、様々な言語で実装し直す手間を省くことができます。

主な特徴としては下記が挙げられます。

  • 多様な言語への変換: Pythonで学習させたモデルを、C、Java、JavaScript、Go、C#、R、PHP、Dart、Haskell、Ruby、F#、Rustなど、様々なプログラミング言語のコードに変換できます。
  • 軽量: 外部の依存関係が少ないため、軽量に動作します。
  • 使いやすさ: シンプルなAPIで、簡単にモデルをコードに変換できます。

m2cgenは、以下のような場合に特に役立ちます。

  • 組み込みシステムへの実装: 組み込みシステムでは、メモリや計算資源が限られているため、軽量なコードでモデルを実行する必要があります。m2cgenを使えば、Pythonで学習させたモデルをC言語などの軽量なコードに変換し、組み込みシステムに実装することができます。
  • Webアプリケーションへの統合: Webアプリケーションでは、JavaScriptでモデルを実行する必要がある場合があります。m2cgenを使えば、Pythonで学習させたモデルをJavaScriptのコードに変換し、Webアプリケーションに統合することができます。
  • パフォーマンスの向上: 特定の言語で実装することで、Pythonで直接モデルを実行するよりもパフォーマンスが向上する場合があります。

m2cgenがサポートするプログラミング言語は下記のものになります。

  • C言語
  • C#
  • Dart
  • F#
  • Go
  • Haskell
  • Java
  • JavaScript
  • PHP
  • PowerShell
  • Python
  • R
  • Ruby
  • Rust
  • Visual Basic (VBA-compatible)
  • Elixir

また、m2cgenは以下のモデルをサポートしています。

分類モデル

タイプライブラリモデル
線形scikit-learnLogisticRegression
LogisticRegressionCV
PassiveAggressiveClassifier
Perceptron
RidgeClassifier
RidgeClassifierCV
SGDClassifier
sklearn-contrib-lightningAdaGradClassifier
CDClassifier
FistaClassifier
SAGAClassifier
SAGClassifier
SDCAClassifier
SGDClassifier
SVMscikit-learnLinearSVC
NuSVC
OneClassSVM
SVC
sklearn-contrib-lightningKernelSVC
LinearSVC
Treescikit-learnDecisionTreeClassifier
ExtraTreeClassifier
Random Forestscikit-learnExtraTreesClassifier
RandomForestClassifier
lightgbmLGBMClassifier(rf booster only)
xgboostXGBRFClassifier
BoostinglightgbmLGBMClassifier(gbdt/dart/goss booster only)
xgboostXGBClassifier(gbtree(including boosted forests)/gblinear booster only)

回帰モデル

タイプライブラリモデル
線形scikit-learnARDRegression
BayesianRidge
ElasticNet
ElasticNetCV
GammaRegressor
HuberRegressor
Lars
LarsCV
Lasso
LassoCV
LassoLars
LassoLarsCV
LassoLarsIC
LinearRegression
OrthogonalMatchingPursuit
OrthogonalMatchingPursuitCV
PassiveAggressiveRegressor
PoissonRegressor
RANSACRegressor(only supported regression estimators can be used as a base estimator)
Ridge
RidgeCV
SGDRegressor
TheilSenRegressor
TweedieRegressor
StatsModelsGeneralized Least Squares (GLS)
Generalized Least Squares with AR Errors (GLSAR)
Generalized Linear Models (GLM)
Ordinary Least Squares (OLS)
[Gaussian] Process Regression Using Maximum Likelihood-based Estimation (ProcessMLE)
Quantile Regression (QuantReg)
Weighted Least Squares (WLS)
sklearn-contrib-lightningAdaGradRegressor
CDRegressor
FistaRegressor
SAGARegressor
SAGRegressor
SDCARegressor
SGDRegressor
SVMscikit-learnLinearSVR
NuSVR
SVR
sklearn-contrib-lightningLinearSVR
Treescikit-learnDecisionTreeRegressor
ExtraTreeRegressor
Random Forestscikit-learnExtraTreesRegressor
RandomForestRegressor
lightgbmLGBMRegressor(rf booster only)
xgboostXGBRFRegressor
BoostinglightgbmLGBMRegressor(gbdt/dart/goss booster only)
xgboostXGBRegressor(gbtree(including boosted forests)/gblinear booster only)

m2cgen の使い方

では、ここからは m2cgen を実際に使用して、機械学習モデルをトランスパイルしてみましょう。初めに、必要なパッケージをインストールします。

# m2cgen をインストール
$ pip install m2cgen

# 今回は scikit-learn のモデルをトランスパイル対象にします
$ pip install scikit-learn

この文章の執筆時点では、m2cgenの更新は数年間行われていません。もし最新版のscikit-learnでエラーが発生する場合は、scikit-learnのバージョンを下げるなどの対応が必要となります。

今回は scikit-learn に同梱されている Irisデータセット を使用して、DecisionTreeClassifierを学習させます。まずは、使用するパッケージをインポートしておきます。

import m2cgen as m2c

from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier

次に、データセットを読み込んで中身を確認しましょう。以下のように、4つの説明変数から構成されています。

datasets = load_iris(as_frame=True)
print(datasets["data"])
     sepal length (cm)  sepal width (cm)  petal length (cm)  petal width (cm)
0                  5.1               3.5                1.4               0.2
1                  4.9               3.0                1.4               0.2
2                  4.7               3.2                1.3               0.2
3                  4.6               3.1                1.5               0.2
4                  5.0               3.6                1.4               0.2
..                 ...               ...                ...               ...
145                6.7               3.0                5.2               2.3
146                6.3               2.5                5.0               1.9
147                6.5               3.0                5.2               2.0
148                6.2               3.4                5.4               2.3
149                5.9               3.0                5.1               1.8

[150 rows x 4 columns]

合わせて、目的変数も見ておきましょう。次のように、3クラスのラベルが付与されていることが確認できます。

print(datasets["target"])
0      0
1      0
2      0
3      0
4      0
      ..
145    2
146    2
147    2
148    2
149    2
Name: target, Length: 150, dtype: int64

通常は、訓練データとテストデータに分割して機械学習モデルを構築しますが、今回はトランスパイルの手順を解説するだけなので、データセットのすべてを訓練データに使用します。

モデルのハイパーパラメータについても、通常は任意の探索アルゴリズムを使用して決定しますが。、ここでは適当に設定します。

tree = DecisionTreeClassifier(
    criterion="gini",
    splitter="best",
    max_depth=None,
    min_samples_split=2,
    max_features="sqrt",
)

# 訓練
tree.fit(datasets["data"], datasets["target"])

# 一応、精度も確認
print(f"Accuracy: {tree.score(datasets['data'], datasets['target'])}")
Accuracy: 1.0

以上でトランスパイル対象の機械学習モデルを準備できたので、ここから様々なプログラミング言語に変換していきます。

まずは、Java のコードを見て見ましょう。

java_code = m2c.export_to_java(
    model=tree,
    package_name=None,
    class_name="Model",
    indent=4,
    function_name="score",
)

print(java_code)
出力されたJavaコード (クリックして展開)
public class Model {
    public static double[] score(double[] input) {
        double[] var0;
        if (input[3] <= 0.800000011920929) {
            var0 = new double[] {1.0, 0.0, 0.0};
        } else {
            if (input[0] <= 6.1499998569488525) {
                if (input[3] <= 1.6500000357627869) {
                    if (input[0] <= 5.950000047683716) {
                        var0 = new double[] {0.0, 1.0, 0.0};
                    } else {
                        if (input[1] <= 2.649999976158142) {
                            if (input[3] <= 1.199999988079071) {
                                var0 = new double[] {0.0, 1.0, 0.0};
                            } else {
                                var0 = new double[] {0.0, 0.0, 1.0};
                            }
                        } else {
                            var0 = new double[] {0.0, 1.0, 0.0};
                        }
                    }
                } else {
                    if (input[2] <= 4.8500001430511475) {
                        if (input[1] <= 3.100000023841858) {
                            var0 = new double[] {0.0, 0.0, 1.0};
                        } else {
                            var0 = new double[] {0.0, 1.0, 0.0};
                        }
                    } else {
                        var0 = new double[] {0.0, 0.0, 1.0};
                    }
                }
            } else {
                if (input[3] <= 1.75) {
                    if (input[2] <= 5.049999952316284) {
                        var0 = new double[] {0.0, 1.0, 0.0};
                    } else {
                        var0 = new double[] {0.0, 0.0, 1.0};
                    }
                } else {
                    var0 = new double[] {0.0, 0.0, 1.0};
                }
            }
        }
        return var0;
    }
}

続けて、Rubyのコードも見てみましょう。

ruby_code = m2c.export_to_ruby(
    model=tree,
    indent=2,
    function_name="score"
)

print(ruby_code)
出力されたRubyコード (クリックして展開)
def score(input)
  if input[3] <= 0.800000011920929
    var0 = [1.0, 0.0, 0.0]
  else
    if input[0] <= 6.1499998569488525
      if input[3] <= 1.6500000357627869
        if input[0] <= 5.950000047683716
          var0 = [0.0, 1.0, 0.0]
        else
          if input[1] <= 2.649999976158142
            if input[3] <= 1.199999988079071
              var0 = [0.0, 1.0, 0.0]
            else
              var0 = [0.0, 0.0, 1.0]
            end
          else
            var0 = [0.0, 1.0, 0.0]
          end
        end
      else
        if input[2] <= 4.8500001430511475
          if input[1] <= 3.100000023841858
            var0 = [0.0, 0.0, 1.0]
          else
            var0 = [0.0, 1.0, 0.0]
          end
        else
          var0 = [0.0, 0.0, 1.0]
        end
      end
    else
      if input[3] <= 1.75
        if input[2] <= 5.049999952316284
          var0 = [0.0, 1.0, 0.0]
        else
          var0 = [0.0, 0.0, 1.0]
        end
      else
        var0 = [0.0, 0.0, 1.0]
      end
    end
  end
  var0
end

最後はC言語にトランスパイルして、実際に生成されたコードを使ってみましょう。

c_code = m2c.export_to_c(
    model=tree,
    indent=4,
    function_name="score",
)

print(c_code)

# コードをファイルに出力
with open("iris.c", "w") as f:
    f.write(c_code)
出力されたC言語のコード (クリックして展開)
#include <string.h>
void score(double * input, double * output) {
    double var0[3];
    if (input[3] <= 0.800000011920929) {
        memcpy(var0, (double[]){1.0, 0.0, 0.0}, 3 * sizeof(double));
    } else {
        if (input[0] <= 6.1499998569488525) {
            if (input[3] <= 1.6500000357627869) {
                if (input[0] <= 5.950000047683716) {
                    memcpy(var0, (double[]){0.0, 1.0, 0.0}, 3 * sizeof(double));
                } else {
                    if (input[1] <= 2.649999976158142) {
                        if (input[3] <= 1.199999988079071) {
                            memcpy(var0, (double[]){0.0, 1.0, 0.0}, 3 * sizeof(double));
                        } else {
                            memcpy(var0, (double[]){0.0, 0.0, 1.0}, 3 * sizeof(double));
                        }
                    } else {
                        memcpy(var0, (double[]){0.0, 1.0, 0.0}, 3 * sizeof(double));
                    }
                }
            } else {
                if (input[2] <= 4.8500001430511475) {
                    if (input[1] <= 3.100000023841858) {
                        memcpy(var0, (double[]){0.0, 0.0, 1.0}, 3 * sizeof(double));
                    } else {
                        memcpy(var0, (double[]){0.0, 1.0, 0.0}, 3 * sizeof(double));
                    }
                } else {
                    memcpy(var0, (double[]){0.0, 0.0, 1.0}, 3 * sizeof(double));
                }
            }
        } else {
            if (input[3] <= 1.75) {
                if (input[2] <= 5.049999952316284) {
                    memcpy(var0, (double[]){0.0, 1.0, 0.0}, 3 * sizeof(double));
                } else {
                    memcpy(var0, (double[]){0.0, 0.0, 1.0}, 3 * sizeof(double));
                }
            } else {
                memcpy(var0, (double[]){0.0, 0.0, 1.0}, 3 * sizeof(double));
            }
        }
    }
    memcpy(output, var0, 3 * sizeof(double));
}

次に、生成したコードを呼び出すコードを準備します。

/*
 * main.c
 */

#include <stdio.h>
#include <stdlib.h>

#define FEATURE_COUNT   (4)     // 特徴量の個数
#define CLASS_COUNT     (3)     // 分類クラスの個数

// 生成されたコードをプロトタイプ宣言しておく
extern void score(double * input, double * output);

static int max_index_of(const double output[]) {
    int index = 0;

    for (int i = 0; i < CLASS_COUNT; i++) {
        if (output[index] < output[i]) {
            index = i;
        }
    }

    return index;
}

int main(int argc, char** argv) {
    double input[FEATURE_COUNT] = {6.2, 3.4, 5.4, 2.3};
    double output[CLASS_COUNT] = {0.0, 0.0, 0.0};

    // 生成されたコードを実行
    score(&input[0], &output[0]);

    // 出力は、それぞれのクラスである確率の推定値となる
    printf("Score: [%f, %f, %f]\n", output[0], output[1], output[2]);
    
    // もっとも大きいインデックスが推定されたクラスに対応する
    printf("Class: %i\n", max_index_of((const double *)&output[0]));

    return EXIT_SUCCESS;
}

次は、これをコンパイルして実行してみましょう。

# GCCでコンパイル
$ gcc -Wall -std=c99 -o main main.c iris.c

# プログラムの実行
$ ./main
Score: [0.0, 0.0, 1.0]
Class: 2

以上のように、学習済みの機械学習モデルがあれば様々なプログラミング言語にトランスパイルできることが確認できました。今回紹介した言語以外に変換したい場合は、GitHub上のコード(m2cgen/exporters.py)を見て使い方を確認して下さい。

なお、トランスパイルの実行で RecursionError が発生する場合は、次のように再帰回数の上限を変更してください。

import sys

# 上限回数は適宜変更のこと
sys.setrecursionlimit(5000)

おわりに

今回は、機械学習モデルを様々なプログラミング言語にトランスパイルできる m2cgen というパッケージを紹介しました。

このパッケージは多くの機械学習モデルをサポートしているものの、近年トレンドとなっている深層学習には対応していません。もし深層学習のモデルをトランスパイルしたい場合、C言語であれば emlearn と呼ばれるパッケージが利用できるので、興味があればご確認ください。なお、emlearn が対応している深層学習モデルは、Kerasの全結合層のみで構築されたものに限定されます。