Go言語」カテゴリーアーカイブ

決定木アンサンブルモデルのデプロイを容易にするライブラリtreeliteの紹介(2)

技術開発部の安井です。前回に引き続き、treeliteの紹介をしたいと思います。今回はtreeliteの予測速度について検証しようと思います。ドキュメント上では2~6倍のスループットが出ると書かれています。この点に関して実際にpython上とgolang上の2つの言語環境で検証しようと思います。

使用データと学習について

今回予測速度を検証する際に使用するデータはAvazuのclick予測のデータセットを使用します。全データ使用すると学習に時間がかかるので、300万件のデータのみを使用して学習を行います。Avazuのデータセットは変数がすべてカテゴリカル変数になっています。今回はこれらの変数をFeeatureHasherを使用して特徴量に変換します。今回FeatureHasherを使用する理由は後述します。FeatureHasherの次元数として今回は16、64、256次元でそれぞれ検証を行います。学習には前回に引き続きLightGBMを使用し、num_boost_roundを100で設定します。これで弱学習器として決定木が100本生成されることになります。学習するときに使用したコードを以下に記述します。

pythonでの予測速度比較

まずはじめにpythonでの予測速度の比較を行います。 予測速度は以下の3パターンのデータ量で比較を行います。

  • instance: 1件
  • batch: 1,000件
  • big batch: 1,000,000件

予測速度は複数回予測したときの平均時間を記載しています。

16次元の場合

16次元の場合、treeliteの方がおおよそ1/2~1/3の時間で予測ができており、概ねドキュメントの記載の通りであることが見受けられます。

64次元の場合

64次元の場合、バッチ予測の場合は16次元と同様にtreeliteの方がおおよそ1/2~1/3の時間で予測ができています。 しかし、instance予測の場合lightgbmとtreeliteの予測速度はそこまで差がないように見受けられます。

256次元の場合

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で測定したベンチマークの結果です。

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でのバッチ予測に関してはスピードアップしていることが確認できました。 最近は予測に特化したライブラリが他にも出てきているので実環境に組み込むといった観点でこういった予測に特化したライブラリもチェックしていきたいと思います。

決定木アンサンブルモデルのデプロイを容易にするライブラリtreeliteの紹介(1)

はじめまして。技術開発部の安井です。現在は弊社で運営しているDSPにおける広告効果の分析や機械学習モデルの構築/運用を行なっています。

DSPと機械学習

詳細なDSPに関する説明は省きますが、DSPではSSPから受け取った広告リクエストに対して以下の事項を決定して広告オークションにおける入札応答を行わなければいけません。

  • 入札可能な広告の内、どの広告で入札を行うのか
  • いくらで入札を行うのか

入札する広告、金額を決定にはクリック率、コンバージョン率等の広告リクエストに対する実際のパフォーマンスの値をを活用しています。これらの広告パフォーマンスを入札サーバ上で予測するために弊社では機械学習モデルを活用しています。しかし、DSPにおける機械学習モデルの活用には以下のようなハードルが存在します。

  • 入札サーバを構築しているプログラムと機械学習モデルを学習しているプログラムでは使用しているプログラミング言語が異なる
  • SSPへの入札応答にはシビアな時間制約が存在する

これらを解決するための方法として今回はtreeliteというライブラリをご紹介します。

treelite

treeliteとは近年巷でよく使われているXGBoostやLightGBM等のGBDTの実装ライブラリやscikit-learnに実装されているRandomForestRegressor(Classifier)、GradientBoostingRegressor(Classifier)等のいわゆる決定木をアンサンブルしているモデルのデプロイを容易にするためのライブラリになっています。加えて、モデルが直接Cコードとして吐き出されるため、推論時間の高速化も期待できる実装となっています。開発は以下のリポジトリで行われています。

dmlc/treelite

Referenceはこちら

Treelite: toolbox for decision tree deployment

treeliteを用いたモデルのデプロイのオプションとして次の3つのオプションが示されています。

1.デプロイターゲットのマシンにtreeliteをインストールする

こちらはデプロイ先のマシンにtreeliteをインストールしておくシンプルな方法です。 ゆえにデプロイ先のマシンにpythonがインストールされている必要があります。

2.デプロイターゲットのマシンにランタイムパッケージと一緒にデプロイする

treeliteではランタイムパッケージをアウトプットすることができます。 このランタイムパッケージをデプロイプロセスで同時にターゲットマシンにデプロイすることで、 モデルを使用できるようにする方法です。 こちらもデプロイ先のマシンにpythonがインストールされている必要があります。

3.モデルとなるCコードのみデプロイする

こちらはCコードとしてアウトプットされたモデルのみをターゲットマシンに送る方法です。 Cコードに変換されたモデルは以下のインタフェースから予測値を取得できます。

dataが予測を行うときに必要となる特徴量の集合で、pred_marginはモデルがアウトプットした値をシグモイド関数に通すか通さないかというフラグとなっています(2値分類タスクの場合)。union Entryに関しては以下のように定義されており、特徴量が存在しない場合はmissingに-1が設定されており、特徴量が存在する場合はfvalueに値が格納されている状態を定義します。qvalueに関しては現状使われていないように見受けられます。

golangでのtreeliteモデルの使用方法

上記3種類のデプロイ方法のうち、3番目のCコードのみをデプロイする方法であればpredict関数とのつなぎ込みの部分さえ実装すればpython以外の言語 でもアンサンブルモデルを使用できます。弊社の入札サーバではgolangが採用されており、cgoを使うことでアンサンブルモデルのつなぎ込みを実現することができます。今回はLightGBMのモデルをtreeliteのモデルに変換し、golangから参照する部分までご紹介します。まずは、LightGBMのモデルを読み込みtreeliteのモデルをアウトプットする部分です。LightGBMのモデルはlgb.modelというファイルに保存されていることとします。

5行目のloadでLightGBMのモデルを読み込み6行目で共有ライブラリに変換を行います。その後、7行目で変換した共有ライブラリから必要なヘッダーとCコード、メタ情報を抽出し、zipファイルにまとめます。デフォルトだとzipファイルには以下のファイルがまとめられています。

header.hmain.cが実際のモデル部分でそれらをコンパイルフローがMakefileにまとまっています。recipe.jsonにはモデルコードのメタ情報が格納されています。それではこれらのモデルをgolangから参照できるようにしましょう。コードはこちらとなります。

ディレクトリ構成は以下のようになっていることを想定しており、こちらのコードはmain.goに記述されています。

こちらのコードは16次元のベクトルを受け取り0~1までの確率値を返すモデルを想定しています。予測を行う関数はfunc predictTreelite(fvals []float32) float32ですがまず最初に受け取ったベクトルをバイナリ列の配列に変換しています。これはunion Entryを表現するための型がgolang側に存在しないためこのような形を取っています。今回はベクトルの中にmissingが存在しない想定で実装がされていますが、missingが存在しうる場合は-1のバイナリ列を代わりに入れてあげるようにします。このようにしてバイナリ列の配列に変換されたベクトルをモデルファイルのpredictに渡してあげることでモデルの予測値を獲得します。実際にビルドして実行すると以下のように0~1までの確率値が出力されます。

さいごに

今回はtreeliteでLightGBMで学習されたモデルをtreeliteのモデルに変換して、golangから参照するところまで紹介しました。次回はtreeliteのモデルとLightGBMのモデルとの予測速度比較、golangから参照したtreeliteモデルとgolangで記述されたアンサンブルモデル予測ライブラリleavesとの予測速度比較を行いたいと思います。

treeliteのドキュメント

SQS + Lambdaでs2sの基盤を作った話

こんにちは。技術開発部・配信/インフラチームの二階堂です。

弊社のプロダクトでは連携している効果測定ツールに対して、S2Sで通知を行う場合があります。 この通知処理を以前まではEC2上に配置したデーモンプログラムで行っていたのですが、処理の共通化・高速化・インフラ費用削減などの目的でSQS+Lambdaに移行しました。 この経験を踏まえて移行時のポイントを紹介したいと思います。

移行前のフローと問題点

  1. 通知先のURLの入ったログを通知用インスタンス(複数ある内のどれか一つ)に書き出す
  2. EC2上に配置したデーモンプログラムがログを読み込み通知を行う
  3. 同じくデーモンプログラムが通知結果のログを書き出す

問題点

  1. 常時起動のインスタンスが高価
  2. 時間帯によって通知量が変化するため通知量が多い時に時々遅延が発生する
  3. 通知漏れが発生した際にどの通知用インスタンスが原因か調査するコストが高い

SQS + Lambda のフロー

  1. 通知先のURLの入ったログをSQSに送信
  2. 定期実行されるLambda(図中a)がSQSに送られたメッセージ数(NumberOfMessageSent)に比例した数の通知用Lambda(図中b)をキック
  3. 通知用LambdaがSQSからメッセージを取得→通知→結果をS3に保存を5分間繰り返す

特徴・改善点

  • 常時起動EC2よりLambdaの方が圧倒的に安価 (問題点1)
  • 全てのログを一度SQSに送る事でそれ以降のフローを共通化し調査コストを下げた (問題点3)
  • Lambdaを2段構成にする事で通知量の変化に柔軟に対応できるようになった (問題点2)
  • 通知用Lambdaはログのパースと通知しかしていないので他の通知にも容易に対応できる

効果

  • インフラコストが約10分の1に減った
  • リリース以前には定期的に発生していた調査や再通知などの対応もほぼ無くなり安定して動作している

移行した感想

Lambdaの料金は実行時間*メモリ使用量なのでその辺を意識したコードになっていないと高い効果が得られません。 今回の処理内容はただの通知なのでメモリはあまり使わないので速度を上げる工夫として通知部分を並列化しました。 最初は並列化していなかったのですがその時は移行前とあまり費用が変わらなかったことを考えるとコスト意識の重要性が判りやすいのではないかと思います。

今回の基盤開発は並列化対応も含めて何かと初めての経験が多い開発だったので効果にしても経験にしてもとても有意義なものだったと思います。

今回は以上となります。

アドサーバの実装にGo言語を用いるメリット

こんにちは。配信・インフラチームの川住です。

先日の記事にもありますが、最近、弊社DSP『Bypass』のRTB入札サーバはGo言語で実装されたものに完全にリプレイスされました。以前の入札サーバはLuaとC言語で実装されていましたが、規模の拡大に伴ってより大量のリクエストを高速に捌く必要が出てきたため、弊社SSP『AdStir』での開発・運用実績があり、Luaより処理が高速で、かつ比較的容易にHTTPサーバを実装できるGo言語へのリプレイスに至りました。

今回は、アドサーバの実装にGo言語を用いるメリットをいくつか紹介します。

リクエスト処理時のリソース消費がNginx+Luaの構成に比べて少ない

従来のLua実装では、ngx_luaモジュールを使用しており、Nginxの各プロセス内でLuaのプログラムを実行する形を取っていました。したがって、比較的大きなサイズのプロセスがforkされてしまうため、メモリ消費量が非常に多くなっていました。それに対して、Go言語を用いたサーバでは、プロセスをforkすることなくリクエストを並行して処理できるため、メモリ消費量がLua実装に比べて非常に少なく済んでいます。

強力なキャッシュモジュールの存在

RTBの入札サーバでは、大量のリクエストを高速に捌く必要があります。そのため、memcachedなどのKVSとの通信にかかるコスト (通信回数, データサイズ etc.) も考慮しなければなりません。Go言語には『go-cache』という強力なインメモリキャッシュのライブラリがあり、こちらを使用することで、KVSから取得したデータをプロセス内に保持でき、KVSとの通信コストを減らせます。Go言語のサーバ自体は1プロセスで動作しているため、キャッシュデータの共有も比較的容易に行えます。

以下にgo-cacheを用いたデータの取得と格納のコードを掲載します。

上記のように、Get関数とSet関数を用いて簡単にデータの取得や格納を行えます。データの格納時にはTTLも設定できます。しかし、go-cacheではデータの取得や格納を行う際にMutexを用いた排他制御を行っています。そのため、RTBの入札サーバのようにデータの読み書きが頻繁な環境で使用すると、go-cacheへのアクセス自体がボトルネックとなるため性能が低下してしまいます。このような環境では、go-cacheのインスタンスを複数生成しておき、キーによってシャーディングするなどして同一資源へのアクセスを分散させる必要があります。以下にシャーディング処理の一例を掲載します。

まとめ

Go言語を用いることで、高速に動作するアドサーバを比較的容易に実装できます。ただし、Go言語のサーバでは1プロセスで処理を行うため、共有資源の排他制御がボトルネックとなる可能性があり、その点を考慮しつつ実装する必要があります。