技術開発部の安井です。前回に引き続き、treeliteの紹介をしたいと思います。今回はtreeliteの予測速度について検証しようと思います。ドキュメント上では2~6倍のスループットが出ると書かれています。この点に関して実際にpython上とgolang上の2つの言語環境で検証しようと思います。
使用データと学習について
今回予測速度を検証する際に使用するデータはAvazuのclick予測のデータセットを使用します。全データ使用すると学習に時間がかかるので、300万件のデータのみを使用して学習を行います。Avazuのデータセットは変数がすべてカテゴリカル変数になっています。今回はこれらの変数をFeeatureHasherを使用して特徴量に変換します。今回FeatureHasherを使用する理由は後述します。FeatureHasherの次元数として今回は16、64、256次元でそれぞれ検証を行います。学習には前回に引き続きLightGBMを使用し、num_boost_roundを100で設定します。これで弱学習器として決定木が100本生成されることになります。学習するときに使用したコードを以下に記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#! /usr/bin/env python import sys import pandas as pd import lightgbm as lgb import treelite import treelite.runtime from sklearn.feature_extraction import FeatureHasher from sklearn.model_selection import train_test_split train = pd.read_csv('input/train', nrows=4000000) dim = int(sys.argv[1]) fh = FeatureHasher(dim) X = fh.transform(train.drop(['id', 'click'], axis=1).to_dict('records')) y = train['click'] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=47) dtrain = lgb.Dataset(X_train, label=y_train) dtest = lgb.Dataset(X_test, label=y_test) params = { 'objective': 'binary', 'metric': 'auc', 'boosting': 'gbdt', 'learning_rate': 0.01, 'seed': 47, } bst = lgb.train( params, dtrain, num_boost_round=100, valid_sets=[dtrain, dtest], valid_names=['train', 'test'], ) lgb_preds = bst.predict(X_test) bst.save_model(f'fh_lgb_dim_{dim}.model') model = treelite.Model.load(f'fh_lgb_dim_{dim}.model', model_format='lightgbm') model.export_lib(toolchain='gcc', libpath=f'./fh_lgb_dim_{dim}.so', verbose=True) pd.DataFrame(X_test.toarray(), columns=['var_{}'.format(i + 1) for i in range(dim)]).to_csv(f'X_test_fh_dim_{dim}.csv', index=False) |
pythonでの予測速度比較
まずはじめにpythonでの予測速度の比較を行います。 予測速度は以下の3パターンのデータ量で比較を行います。
- instance: 1件
- batch: 1,000件
- big batch: 1,000,000件
予測速度は複数回予測したときの平均時間を記載しています。
16次元の場合
1 2 3 4 5 6 |
lightgbm instance: 0.00010517575740814209s treelite instance: 0.00003566386699676514s lightgbm batch: 0.0007027374267578125s treelite batch: 0.0003089529752731323s lightgbm big batch: 0.6247398257255554s treelite big batch: 0.2774349188804626s |
16次元の場合、treeliteの方がおおよそ1/2~1/3の時間で予測ができており、概ねドキュメントの記載の通りであることが見受けられます。
64次元の場合
1 2 3 4 5 6 |
lightgbm instance: 0.0001033491849899292s treelite instance: 0.00009161360263824462s lightgbm batch: 0.0007573297739028931s treelite batch: 0.00035665130615234375s lightgbm big batch: 0.79298579454422s treelite big batch: 0.32156704187393187s |
64次元の場合、バッチ予測の場合は16次元と同様にtreeliteの方がおおよそ1/2~1/3の時間で予測ができています。 しかし、instance予測の場合lightgbmとtreeliteの予測速度はそこまで差がないように見受けられます。
256次元の場合
1 2 3 4 5 6 |
lightgbm instance: 0.00010415022373199463s treelite instance: 0.0003009482145309448s lightgbm batch: 0.0011013625144958496s treelite batch: 0.00042464187145233154s lightgbm big batch: 1.5125486660003662s treelite big batch: 0.38220595598220825s |
256次元の場合、バッチ予測の場合はtreeliteの方が1/3~1/4の時間で予測ができており、treeliteの効果が最も出ていると考えられます。 対象的に、instance予測の場合はtreeliteの方が予測に3倍の時間がかがっていることがわかります。
まとめ
バッチ予測の場合は概ね期待している効果が出ていますが、インスタンス予測の場合は予測する特徴量のサイズが増えるほどの効果が出ないばかりか、 悪化する場合も存在します。これに関してはtreelite側のメソッドがバッチ予測の場合とインスタンス予測の場合で異なることが原因として考えられます。
- バッチ予測: treelite.Predictor.predict(batch, verbose=False, pred_margin=False)
- インスタンス予測: treelite.Predictor.predict_instance(inst, missing=None, pred_margin=False)
この2つの違いに関してですが、batchの方はtreelite側で処理をしやすいようなデータ型を期待していますが、instの方ではnumpy.ndarrayやscipy.sparse.csr_matrix等のarray objectを期待しています。この差により予測速度に差が出ているのではないかと想像していますが、詳しくは引き続き調査を続けていきたいと思います。
golangでの予測速度比較
golangでの予測速度比較にはtreeliteで生成したモデルコードの予測と以下のGBDT予測ライブラリの予測での速度比較を行います。
このライブラリはXGBboost/LightGBm/scikit-learnで学習したGBDTのモデルをロードしてgolang上で予測を行えるようにしたライブラリです。 こちらはインスタンス予測のみを比較することとします。 以下がgolanで測定したベンチマークの結果です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
16次元の場合 $ go test -bench . -benchmem goos: linux goarch: amd64 pkg: treelite_bench/dim16 BenchmarkPredictTreelite-8 2000000 849 ns/op 64 B/op 1 allocs/op BenchmarkPredictLeaves-8 500000 3655 ns/op 8 B/op 1 allocs/op PASS ok treelite_bench/dim16 4.426s 64次元の場合 $ go test -bench . -benchmem goos: linux goarch: amd64 pkg: treelite_bench/dim64 BenchmarkPredictTreelite-8 2000000 637 ns/op 256 B/op 1 allocs/op BenchmarkPredictLeaves-8 500000 3546 ns/op 8 B/op 1 allocs/op PASS ok treelite_bench/dim64 3.717s 256次元の場合 $ go test -bench . -benchmem goos: linux goarch: amd64 pkg: treelite_bench/dim256 BenchmarkPredictTreelite-8 2000000 890 ns/op 1024 B/op 1 allocs/op BenchmarkPredictLeaves-8 300000 4220 ns/op 8 B/op 1 allocs/op PASS ok treelite_bench/dim256 3.981s |
BenchmarkPredictTreelite-8がtreeliteで予測した場合のベンチマークで、BenchmarkPredictLeaves-8がleavesで予測した場合のベンチマークです。 treeliteの予測モデルのほうが1/4~1/6の時間で予測が行えれていることがわかります。 これを見ると動的にロードしたleavesの予測モデルより、はじめに実行バイナリに組みこんでおいたtreeliteの予測モデルのほうがパフォーマンスが良いことが わかります。
treelite使用上の注意
冒頭でも述べましたが今回はカテゴリカル変数をFeatureHasherを使用して学習するときに使用する特徴量を作成しました。 しかし、LightGBMではカテゴリカル変数をそのまま特徴量として入力することができるようになっています。(厳密にはint型のidへの変換は必要) 今回カテゴリカル変数をそのまま学習に用いなかった理由としてはLightGBMでカテゴリカル変数を使用して学習を行うとtreeliteの予測結果と異なってしまうという事象が発生したためです。 しかし、現在では以下のissueによってこの問題も解決されたようです。
https://github.com/dmlc/treelite/issues/77
実際手元の環境でカテゴリカル変数を用いた場合でもLightGBMの予測モデルとtreeliteの予測モデルの結果が一致することを確認しました。 古いバージョンのtreeliteを使用する場合はご注意ください。
さいごに
今回はtreeliteの予測速度性能に関してpython, golang上で比較を行いました。 golangでの予測、pythonでのバッチ予測に関してはスピードアップしていることが確認できました。 最近は予測に特化したライブラリが他にも出てきているので実環境に組み込むといった観点でこういった予測に特化したライブラリもチェックしていきたいと思います。