カテゴリー別アーカイブ: Go言語

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

Pocket

はじめまして。技術開発部の安井です。現在は弊社で運営している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の基盤を作った話

Pocket

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

弊社のプロダクトでは連携している効果測定ツールに対して、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言語を用いるメリット

Pocket

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

先日の記事にもありますが、最近、弊社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プロセスで処理を行うため、共有資源の排他制御がボトルネックとなる可能性があり、その点を考慮しつつ実装する必要があります。