有料集客の予算推定モデルの実運用開始までの道のり

# 前提・背景

この記事は[LIFULL Advent Calendar 2021](https://qiita.com/advent-calendar/2021/lifull)の3日目の記事です。

この記事では、ある予算最適化の計算アルゴリズムをAIのチームから引き継いで、システムに組み込むまでをまとめています。

LIFULLでは社内用のマーケティング自動化ツール（プロダクト名は**MAM（マム）**）を内製しており、私はその開発チームに所属しています。詳細は同僚のマーケターの丸田さんが書いた「[マーケティングオートメーション｜CRMではない、MAを考えてみた。」](https://note.com/homes_marketing/n/nee4962936df2)をいただきたいのですが、例えば以下のような機能があります。

> ①独自の広告評価指標を用いた実績を可視化
> ②いくら広告宣伝費を使えば成果が最大化するかのシミュレーション
> ④広告配信の費用対効果を高めるため自動で入札単価を調整

この記事は、この「**②いくら広告宣伝費を使えば成果が最大化するかのシミュレーション**」のシステムについての記事です。

LIFULLは不動産ポータルサイトの[LIFULL HOME'S](https://www.homes.co.jp/)の運営企業で、集客チームでは毎月各マーケット（賃貸、分譲マンション、分譲戸建、流通、注文住宅…）の広告予算を決める必要があります。このシステムでは、各マーケットで次のようなグラフを出し、利益最大となるコスト（広告予算）を計算し、マーケターの予算決定のための情報として利用してもらっています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127762/ed93aab5-3500-fe06-72b6-0f8564dd07fa.png" width=600 />

※上記グラフは架空のものです。


# 自分が入った段階の状態

上記のグラフを描画するために、おおまかには以下のような戦略でアルゴリズムは実装されています。詳細は[Ltech#11 不動産領域のAI活用最前線 〜初完全リモート開催〜　開催レポート](https://www.lifull.blog/entry/2020/11/02/203126)にある谷山さんの[発表資料](https://www.slideshare.net/next_developer/ltech11)をご覧ください。


1. ガウス過程回帰で学習を行い、「日付と予算を入れると売上を予測する関数」を学習する
1. 各日付　$i$の予算$x$と売上$y$を単純な曲線 $y = \frac{a_ix}{x + b_i}$　で近似する
1. 一ヶ月で見たときの最適な予算分配が解析的に解ける

ただし、以下のような問題が残っていました。（実際には最初から整理できていたわけではなく、多マーケット展開する際に問題が発覚したものが多いです）

1. Ruby on Railsのサンプルアプリケーションが実装されていたが、そのまま既存の管理画面に組み込めない状態だった
1. 時系列モデル単体で見たときの精度評価はされていたが、グラフ描画まで含めた評価はされていなかった
2. サンプルでは単体のマーケットで実装されていたが、「予算はマンションだが、売上は他のマーケットも合計したもので見たい」という要件があり、複数モデルを組み合わせる方法が必要だった
3. アルゴリズム（ガウス過程回帰）が複雑で、計算時間やロジック面の調整などの問題がある


それぞれ、以下のような対処を行いました。

1. 複雑な計算処理をバッチ処理として切り出し、管理画面からはバッチ処理の起動と結果の描画のみを行うようにした
1. 精度評価を設計して実施し、学習のepoch数を増やした
1. マーケターが最も興味のある利益最大点が一致するように近似した
1. ドメイン知識を入れ、より単純な時系列モデルで置き換える（※これはまだ実装中の問題です）

これらの問題にどう対処していったかを説明します。

# アプリケーションに組み込むまでの道のり

一人でやった風に見えますが、実際にはマーケター（＝自分たちから見るとユーザー）との仕様確認で前述した丸田さんや、Railsの管理画面は他のエンジニアの実装してもらっています。

## ①計算処理をバッチ処理として切り出した

まず、集客の開発チーム内で管理できるように再設計し、実装を進めました。

当初はRailsのアプリケーション内で[PyCall](https://codezine.jp/article/detail/10251)でPythonを呼び出して複雑な計算処理をさせていた点や、データを保持するBigQueryの呼び出しでの待ち時間がある点、それを防ぐためにアプリケーション内にcsv形式でデータを持つような不適切な形で作り込まれていた点などが問題でした。また、メモリリークがあるらしく、しばらく立ち上げているとアプリケーションが落ちる状態でした。

以下のような3つのコンポーネントに整理でき、このうちの2.が3.のサーバーに同居していて不要に複雑になってしまっていたため、「管理画面上から2.の処理を担当するバッチ処理を起動と、描画に専念し、計算ロジックはPythonのバッチ処理で完結させる」ようにしました。

1. 各日付 $i$ の曲線のパラメータ( $a_i, b_i$ )を推定するバッチ処理
2. グラフを描画するための数値計算ロジック
3. グラフの描画を行う管理画面

この時点ではアルゴリズムの中身は深追いせずに、ただIOとデータフローを整理して、無難な設計にすることを目指していました。


## ②パイプライン全体での精度評価を実施した

時系列モデル単体での評価はされていたのですが、「マーケターが利用するときにどんな懸念があるのか」という観点での精度設計と検証は実施できていませんでした。

具体的には、実装を進めていく中でモデルの学習時のブレが問題になり、その「学習時の偶然でどれだけ意思決定に必要な値がぶれてしまうのか」を検証するため、「100回学習させて利益最大点がどれだけブレるのか検証する」ような実験を実施しました。その結果、ブレが思いの外大きく対処に追われた時期があります。

学習曲線を見たところ、まだepoch数を増やすと精度改善の余地があることが分かり、そこを増やすことで対処できました。ただし、これで20分程度で終わっていた学習が2時間かかるようになり、マーケットの横展開やコストの問題が心配されるようになってしまいました。

* 各モジュール単位ではなく、最終出力に近いところでどういう懸念があるのかテストする必要がある
* 精度の基準を作る際には「実利用時に問題になりそうなこと」の知識が必要なので、きちんと懸念を伝えてコミュニケーションを取る必要がある。具体的には集客チーム側が、確率的なアルゴリズムで計算結果ごとにブレが生じることを把握していなかった。
* 「データを出力してマーケターに手作業で確認してもらう」という形にしてしまったため、人への負荷も大きかった

ただ、ここの統計や機械学習のシステムのテスト手法は、今後も勉強していく必要がありそうです。


## ③複数マーケットをまとめる近似計算を実装した

複数マーケットを組み合わせる際、「予算はマンションだが、売上は他のマーケットも合計したもので見たい」という要件があり、複数モデルを組み合わせる方法が必要でした。具体的には複数マーケットを組み合わせようとすると、

```math
y = \frac{a_{分譲マンション}x}{x + b_{分譲マンション}} + \frac{a_{分譲戸建}x}{x + b_{分譲戸建}} + ...
```

という形になり、当初の方法では解析的に解けませんでした。これについては、3つの解決方法を考えていました。

1. 解析的に解ける方法を見つける（※見つからなかった）
1. 最初から全対象マーケットの売上合計値で予測モデルを作る
2. ソルバー（`scipy.optimize`）を使って厳密解を計算するよう実装する
3. 何らかの方法で　$y = \frac{a_{全体}x}{x + b_{全体}}$に近似する

最初に２.を考えたのですが、マーケターのユースケースを洗い出すと、各マーケットで分解した数値で見たい場合があり、その全部の組み合わせは膨大で管理や実装が大変だったために棄却しました。そしてソルバーは実際に試したところ計算が遅く、対象マーケットが多い場合にうまく解けなかったため、近似計算を考案して実装しました。ただし、初期値の選び方やアルゴリズムの選定でも対処できたかもしれません。

当初は $x=0$ 付近の傾きと $x \rightarrow \infty$ のときの値を一致させるように以下の計算をしていたのですが、誤差が大きい場合があり、この方法ではうまくいきませんでした。

```math
a_{全体} = a_{分譲マンション} + a_{分譲戸建} +... \\
\frac{a_{全体}}{b_{全体}} = \frac{a_{分譲マンション}}{b_{分譲マンション}} + \frac{a_{分譲戸建}}{b_{分譲戸建}} + ... 
```

そこで代わりに「二分探索（`scipy.optimize.bisect`）で利益の関数の微分が0になる利益最大点を探し、その$x$と$y$が一致するような$a_{全体}$と$b_{全体}$を選ぶ」ような計算方法に変えたところ、うまくいきました。実際にはコストが損益分岐点を越えるあたりから誤差が大きい場合があるのですが、実運用で興味があるのは損益分岐点付近であるため問題はありません。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127762/623d7d7c-1103-aaed-5e5c-01d93b70ffc6.png" width=360 /><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127762/67c95447-2323-1a66-2b9c-c1315522414e.png" width=360 />

これでひとまず多マーケット展開できる状態になりました。

## ④シンプルなモデルに置き換えている（※現在着手）

現在は、既存のモデルの運用が難しく感じており、ガウス過程回帰のモデルを代替する、より単純なモデリング方法を実装しようとしているところです。「②精度評価を実施した」で手間取った反省もあって、検証基準とスクリプトを実装して、それを通るようなモデルを実装するTDD風(?)なやり方をしようとしています。

1. epoch数を増やしたことで計算時間が長く、多マーケット展開する際の支障になっていた
2. 実運用より異常に大きな値を計算するマーケットがあったが、既存のロジック上で対処しづらかった

既存のモデルはほとんどブラックボックスなため、2.のような問題があったときに、ロジック（例えば最適化関数を工夫するとか）を工夫して対処できることが少ないです。今の所、「本当に実際にもっと投資できるならリターンが得られる広告媒体が有るが、それに追加投資するのが難しい」という状態じゃないかと推測しています。

これがもし成功すれば、他の応用先にもよりスムーズに適用できると考えています。


# 最後に

現状は「一応使い始めてもらっている」状態で、さらなる横展開や使い勝手の改善などがあります。安心して使ってもらえるプロダクトにするために精進しようと思います。

実は弊チームはボスを募集しているので、こういう数値計算やMarketingOps的な内容に詳しいマネージャーがいたらぜひ応募していただけたら嬉しいです🙇

[アプリケーションエンジニア（マネジャー）【LIFULL HOME'S（デジタルマーケティング領域）】](https://hrmos.co/pages/lifull/jobs/010-0055)