タクシーの配車アプリ「GO」を担当

鈴木隆史氏:OptunaをKuberflow Pipeline上に乗せて、分散ハイパラ(ハイパーパラメーター)チューニングをした話をします。

自己紹介をします。私は、Mobility Technologies株式会社のMLエンジニアグループで、エンジニアをやっている鈴木です。もともと2019年に株式会社ディー・エヌ・エーに中途入社して、機械学習基盤のパイプラインやインフラ系の整備をメインに担当していました。2020年に担当していた部署が「Mobility Technologies」になり転籍しました。

私たちの会社でメイン事業となっているのが、タクシーの配車アプリの「GO」です。こちらはもともと「JapanTaxi」とDeNAの「MOV」という配車事業が統合したものです。現在使っている方がいるかもしれませんが、アプリで気軽に配車のリクエストができるほか、明日や1週間後のように時間を指定する機能もあるので、ぜひご利用ください。

今日話す内容は、「OptunaとKuberflow Pipelineを利用した並列ハイパラチューニングのシステム」と、コードの解説がメインです。

需要供給予測と走行ルートの推薦をベースとしたタクシードライバー向けサービス「お客さま探索ナビ」

今回Optunaの導入に至った背景を伝えるために、まずは私たちのプロダクトについて説明します。「お客さま探索ナビ」というプロジェクトで経路の推論をしていた中で、機械学習のコンポーネントのパラメーターを最適化したいという要望があり、Optunaを導入するに至りました。

お客さま探索ナビについて簡単に説明します。これは機械学習システムやモデルの評価を行っている、タクシードライバー向けのサービスです。今までは探索ルートをドライバーの勘や経験に頼っていましたが、機械学習によって需要と供給を予測して、最適な営業経路を提案することで空車になる時間を短縮し、歩合制の契約が多いタクシードライバーの収入を安定化させる目的で作られています。

お客さま探索ナビは、「需要供給予測」と「走行ルートの推薦」の2つのコンポーネントで実装されています。1つ目の需要供給予測は、交通状態など直近のトレンドを考慮するために機械学習モデルを利用しています。GOというアプリに蓄積されているプローブデータ(実際にタクシーが走って取得したデータ)から、直近の乗車数や周辺の乗車数、統計値などをモデルのインプットとして渡して、次の30分に発生する乗車数をMLモデルで予測しています。

2つ目が、走行ルートの推薦です。ここでは強化学習を利用して方策を求めています。ご存じの方が多いと思いますが、ざっくり言うと強化学習とは、環境との試行錯誤を通じて累積報酬を最大化する方策を獲得する方法です。エージェントが環境を観測し、それに基づいて行動を起こし、そこで得られた報酬によって行動を調整していく仕組みです。GOでの「行動」は流しのドライバーの経路で、実際にどれくらいお客さんをピックアップして収益を得られたかを「報酬」としています。

Optunaを「お客さま探索ナビ」に導入した経緯

今説明した強化学習のコンポーネントには「Value Iterator」というものが用いられています。ざっくりとですが、このValue Iteratorにも複数のパラメーターが存在していて、需要と供給量だけではなくハイパーパラメーター群によっても推薦されるルートが変化します。ルートが変化すると当然、シミュレーターで得られる営業収益、ひいては実際のドライバーの営業収益にも変化があります。

こういった背景のもと、チーム内でValue Iteratorのハイパラをチューニングしたいという声が挙がっていました。このハイパラは非常にたくさんあって、今までは「アルゴリズムエンジニアの経験に基づいた値」を決定した結果、採用されて運用していました。ただ、お客さま探索ナビはいろいろな地域にリリースすることになっているので、地域ごとにモデルが違ったり、最適なパラメーターが異なったり、時間が経つにしたがって最適なパラメーターが変わってきたりするケースが考えられます。

そのため今回Optunaを導入しましたが、その前に、お客さま探索ナビのモデルの評価とシミュレーションについて説明します。先ほど言ったとおり、2つのMLのモデルを用いて、道路ごとの需要と供給量の2つを予測して、その値から強化学習を用いて経路を提案しています。需要とは、ユーザーがどれくらい乗りたいかという情報です。供給量とは、今どれくらいのドライバーがそのエリアにいるかという情報です。

実際にその経路に従って走った場合、どれくらいの営業収益が得られるかを見るためにシミュレーターを開発しました。GOを使ったユーザーの需要や供給の過去のデータがログとして残っているので、それを使って経路推薦した道と照らし合わせて、実際そこにお客さまが存在するのかを評価しています。

モデルを更新する際には、単純なRMSEなどで誤差を評価しますが、今はこのシミュレーターを使って、実際にどれくらいの収益が出そうかを、新旧パラメーターを比較してデプロイするような判断基準を持って運営しています。

機械学習に特化したワークフローエンジン「Kubeflow Pipeline」を利用

実際にどのようなシステムで開発しているかを説明します。まず、Kubeflowです。これはGoogleが開発しているワークフローのシステムで、Kubernetes上の機械学習のライフサイクルを一通り実行するためのツール群が揃っています。ちなみに、今回はKubeflow Pipelineを使い、ほかのコンポーネントは使っていません。Kubeflow Pipelineは、特に実験環境などで使いやすい機械学習に特化したワークフローエンジンです。

ワークフローエンジンというと、ApacheのAirflowやDigdagが有名ですが、Kubeflow PipelineにはよりMLに特化した便利な機能が揃えられています。例えば、タスクごとにインプット・アウトプットの可視化や、出力したJupyter NotebookやConfusion MatrixなどのWeb UI上での可視化が可能です。ただ、うちのプロダクトは、機械学習系の実験のパイプラインに関してはKubeflowを使い、実際本番のパイプラインに関してはAirflowを使うという使い分けをしています。

(スライドを指して)営業収益のコードは、このようになっています。目的関数を定義して、それをチューニングしています。今回は営業収益を最大化したいので、directionはmaximizeに設定しています。

次のストレージも、InMemoryと外部DBの2つのストレージから選択できます。今回私たちは分散学習を実施したかったため、「RDB Storage」を選択しています。永続化によってチューニングの単位で中断や再開が可能なことが、いろいろと便利でした。

Optunaのチューニングフロー

(スライドを指して)システムフローを簡単に、図を使って説明していきます。Optunaはローカルで実行することも可能ですが、今回は分散学習ということでクラスターを用意し、KubeflowのクラスターでOptunaのジョブを管理するようにしています。また、ストレージは永続化のためにCloud SQLを使っています。

まずは、パイプラインクラスターにOptunaのジョブをデプロイします。デプロイされたOptunaのジョブでは新規チューニングを行うため、create_study()を実行しています。Cloud SQLにサーバーを用意しているので、実験のストレージはそちらに接続するようにしています。次に、study.optimize()で目的関数を実行していきます。今回のプロジェクトでは、5つのスレッドに並列でジョブを実行しています。これはTrialが独立しているので、並列でチューニングができるようになっています。

目的関数は、先ほども言いましたが、新しくサジェスチョンされたハイパーパラメーターを使ってシミュレーターを実行すると、どれだけ営業収益が得られるかが報酬として返ってくるので、それを最大化していくといったフローです。シミュレーターが完了するまでは、各スレッドが待機するようになっています。これは負荷対策の基本です。スレッドごとにTrialを回しているので、先にシミュレーターの実行が終わったジョブに関しては、また新しいスレッドからジョブが実行されるようになっています。

そして、Trialが終わった結果をストレージに格納して次のTrialに進んでいます。それが終わると、別のスレッドのものを含むTrial情報から新しいパラメーターをサジェスチョンしています。これ以降は、この流れの繰り返しになりますが、シミュレーターの営収が最大化収束するまでこちらのジョブが実行されています。今回はRDB Storageを使っているので、もし100回の試行が終わっても収束していない場合は、Optunaのload_study()という関数から再開することができます。

(次回へつづく)