🏋️‍♀️

Prophet を Cross Validation して、汎化性能を可視化する

2024/12/23に公開

■ はじめに

データシステム部 推薦基盤ブロックの上國料(かみけん)です。主に ZOZOTOWN のパーソナライズを実現する推薦システムの開発・運用を担当しています。

先日、KPIのモニタリング自動化と運用体制の整備というタイトルで、推薦システムの KPI を自動でモニタリングする方法についてテックブログで紹介しました(自分が書いてはないです)。この中で、異常値の検知に Prophet を採用していることに触れましたが、今回はその課題および「異常検知モデルの精度のモニタリング」に取り組んだ内容について簡単にご紹介します。

■ 概要

推薦システムの KPI モニタリングにおいて、アラートが発生した異常値について「モデルの汎化性能の問題」なのか「実測値が異常値」なのかを即座二判断できないという課題がありました。この問題を対象するために、Cross Validation を実装し、Coverage・MAPE という評価指標の導入を行いました。

まだ prd 環境で施行中ですが、これらの指標を導入することで、異常値が検出された際にモデルの性能をより正確に評価し、適切な対応を取ることが可能になると考えています。

■ 既存のモニタリング方法と課題

● 既存のモニタリング方法

現在、推薦システムの KPI モニタリングには meta が開発した時系列予測ライブラリ「Prophet」を使用しています。Prophet により予測値(yhat)と信頼区間(yhat_loweryhat_upper)を算出し、実測値が信頼区間を外れた場合に異常としてアラートを発生させています(この具体的な実装方法はこちらの元の記事を参考にしてください)。これを各週でアラート対応担当者を決め対応しています。

● 課題点

しかしこの方法には、アラートが発生した際に、それが「モデルの汎化性能の問題」なのか「実測値の異常」なのか迅速に判断できません。このため、システムの信頼性が低下し、アラートが無視されるリスクがあるという課題がありました。

● アプローチ

これらの課題を解決するために、以下の対策を実施しました。
Cross Validation によって複数の日における Prophet の汎化性能を定量化し、アラートが発生した際に、それが「モデルの汎化性能の問題」であるか否かを判断できるようにしました。

  • Cross Validation を導入
  • Coverage と MAPE を評価指標として導入

■ Cross Validation の説明

● Cross Validation とは

Prophet のライブラリが提供している Cross Validation は、時系列データにおける予測モデルの精度を評価するための手法です。過去のデータを異なるカットオフ日(予測を開始する基準日)で分割し、それぞれのカットオフ日でモデルを訓練・予測することで、モデルの汎化性能(未知のデータに対する予測力)を検証します。時系列データでは、データが時間順に並んでいるため、ランダムな分割ではなく、時間に沿った分割が行われます。

Prophet のライブラリが提供している cross_validation 関数は以下のソースコードを見ると理解できます!
https://github.com/facebook/prophet/blob/0bf05baf3c4ace5e02e18a432ec6c2d734a97034/python/prophet/diagnostics.py

以下簡単に cross_validationperformance_metrics の説明を行います。

cross_validation 関数の処理の流れ

主な処理の概要は以下です。

① カットオフ日の生成

  • 引数 cutoffs が与えられていない場合、カットオフ日を生成する。
    • 最初のカットオフは「データの最大日数 - horizon」
    • そこから period 分ずつ遡り、トレーニング用のデータ(initial)が保証されるようにする。(initial が最後に算出されるものの、テストパターンの第 1 タームで使用されるので注意
  • カットオフ日が与えられた場合、データの最小最大範囲に違反しないことを確認

https://github.com/facebook/prophet/blob/0bf05baf3c4ace5e02e18a432ec6c2d734a97034/python/prophet/diagnostics.py#L20-L58

② Cross Validation 実行

各カットオフ日について次の処理を行う:

  • データの分割: カットオフ日以前のデータをトレーニング用として使用し、カットオフ日以降の horizon 分の期間を予測
  • モデルの予測: トレーニングデータでモデルを再学習し、予測を行う。
  • 結果の格納: 予測値、実測値、カットオフ日を pd.Dataframe に格納する。

https://github.com/facebook/prophet/blob/0bf05baf3c4ace5e02e18a432ec6c2d734a97034/python/prophet/diagnostics.py#L156-L205

cross_validation 関数のテストパターンを制御する引数は以下👇

パラメータ 意味
initial テストパターン(第 1 ターム)のトレーニング期間 pandas.Timedelta
period テストパターンと次のテストパターンの差分 pandas.Timedelta
horizon 各テストパターンの長さ pandas.Timedelta
cutoffs 各テストパターンの開始日時の直前(指定しなければ initialperiodhorizon から算出される。) list of pd.Timestamp
parallel 並列処理の方法:
- None: 並列処理なし・デフォルト
- processes: concurrent.futures.ProcessPoolExecutor を使用
- threads:ThreadPoolExecutor を使用(GILの影響で速度は遅い可能性あり)
- dask: Dask を使ってタスクを分散実行(Dask クライアントが必要)または .map メソッドを持つカスタムオブジェクト
str またはカスタムオブジェクト

引数がどのように使われるかのイメージは以下👇

performance_metrics 関数の処理

performance_metrics 関数は、Cross Validation の結果からモデルの予測性能を評価するための各種指標を計算します。これによって、モデルの汎化性能を定量的に評価し、異常値検知のシステムの信頼性を担保できると考えます。df(Cross Validation の結果) 以外デフォルト値が設定されているため、簡単に使用することができます。

① 引数

performance_metrics 関数のテストパターンを制御する引数は以下👇

パラメータ 意味
df クロスバリデーションの結果を含むデータフレーム pd.DataFrame
metrics 計算する性能指標のリスト。指定しない場合はデフォルトで
['mse', 'rmse', 'mae', 'mape', 'mdape', 'smape', 'coverage'] が使用される
list of str, optional
rolling_window ローリングウィンドウのサイズをデータの割合で指定。
平均化に使用するデータの割合。デフォルトは 0.1 (10%)
float, optional
monthly True にすると、ホライズンを月数として計算。デフォルトは False bool, optional

② 指標

performance_metrics 関数で計測することのできる指標郡は以下👇

指標名 説明
MSE (Mean Squared Error) 予測値と実測値の差の二乗の平均。誤差が大きいほどMSEの値も大きくなる。
RMSE (Root Mean Squared Error) MSEの平方根。元のデータと同じ単位で誤差を評価できる。
MAE (Mean Absolute Error) 予測値と実測値の絶対差の平均。異常値の影響を受けにくい指標。
MAPE (Mean Absolute Percentage Error) 予測値と実測値の絶対差を実測値で割ったものの平均。相対的な誤差を評価。
実測値がゼロに近い場合は計算が不安定になる。
MDAPE (Median Absolute Percentage Error) MAPEの中央値。外れ値の影響を受けにくい指標。
SMAPE (Symmetric Mean Absolute Percentage Error) 予測値と実測値の絶対差を予測値と実測値の平均で割ったものの平均。
MAPEよりもバランスの取れた指標。
Coverage 信頼区間(yhat_lower ~ yhat_upper)内に実測値が含まれる割合。信頼区間の適切性を評価。

余談ですが、個人的に指標の取る値の上限と下限が 0 ~ 100% で決められているもののほうが担当者がアラートに対応する際にモデルの汎化性能を捉えやすいと思っています。
なので、KPI モニタリングでモデルの汎化性能を評価する際には、MAPE やその派生指標であるMDAPE、SMAPE、Coverage がおすすめです。

■ 実装

実装内容を簡単に説明していきます!

● トレーニングとテストデータを分割し、Prophet を訓練して予測

train, test = target[:-1], target[-1:]
m = Prophet(interval_width=config.interval_width, holidays=prophet_special_days)
m.fit(train)
future = m.make_future_dataframe(periods=config.prediction_periods, freq=config.freq)
forecast = m.predict(future)

この部分は、時系列データを使って、未来を予測するためのコードです(ZOZO では、毎日の売上や imp、クリックを使用しています)。

Prophet の初期設定として、interval_width で信頼区間の幅を設定します。予測値の中で何 % の範囲を信頼できる値とするかを決めるものです。この範囲外は「異常値」と見なす設定にしています。また、holidays に祝日やイベント日のリストを入れ、モデルがイベントや季節性の影響を考慮して予測できるようにします。
fit で分割したコードをモデルに訓練させます。
make_feture_dataframepredict (予測) の設定を行いますが、periods でどのくらいの未来を予測するかをいれます。freqperiods の予測範囲が日数なのか週数なのかを定義します。(e.g. periods = 1、freq = "D"、だと 1 日予測)

● Cross Validation でモデルの汎化性能の確認

いよいよ、前述した Cross Validation でモデルの汎化性能を測る部分です。モニタリングする指標として、MAPE と Coverage を選択しました。(指標が多すぎると視認性が下がるので、理解が比較的簡単な 2 つの指標にしました!)

以下のコードで、 Cross Validation で各カットオフ日でのパフォーマンスを rolling_window を用いて平滑化した交差検証の性能指標を計算します👇

# クロスバリデーションのためのデータ期間を計算
data_duration = (train["ds"].max() - train["ds"].min()).days
if data_duration < 0:
    raise ValueError("Data duration must be more than 0 days")
calculated_initial = int(data_duration - config.cross_validation_days)
if calculated_initial < 0:
    raise ValueError("Cross validation days exceed data duration")

# クロスバリデーションの実行
df_cv = cross_validation(
    m,
    horizon=f"{config.cross_validation_horizon} days",
    period=f"{config.cross_validation_periods} days",
    initial=f"{calculated_initial} days",
    parallel="processes",
)
df_perf = performance_metrics(df_cv)

# 交差検証の性能指標を計算
mape = df_perf["mape"].mean()
coverage = df_perf["coverage"].mean()

● 信頼区間を使用して異常値検知し、 メッセージを生成

以下のように、実測値が信頼区間からはみ出した場合に、その実測値を異常値としてアラートを生成します。

# 実測値と予測値の取得
actual = test["y"].iloc[-1]
lower = forecast["yhat_lower"].iloc[-1]
upper = forecast["yhat_upper"].iloc[-1]

# アラートの作成
alert_triggered = False
message = f"*{monitor_field}*: "

if actual < lower:
    alert_triggered = True
    message += f"Actual value ({round(actual, 2)}) is below the lower bound ({round(lower, 2)}) in *{config.monitor_table_id}*.\n"

elif actual > upper:
    alert_triggered = True
    message += f"Actual value ({round(actual, 2)}) exceeds the upper bound ({round(upper, 2)}) in *{config.monitor_table_id}*.\n"

message += (
    f"\n*Model's Performance Metrics (CV on {cross_validated_count} data points):*\n"
    f"- Coverage: {coverage:.2%}\n"
    f"- MAPE: {mape:.2%}\n\n"
    f"▶︎ <{config.documentation_link} |Documentation>\n"
    f"▶︎ <{config.dashboard_link}|Dashboard>"
)

■ 結果

アラートは以下のように、Slack 上に送られます。以下のようなアラートでは、若干信頼区間から外れていますが、Coverage の値が十分でないので、実測値に問題がある可能性が高いです。このように、アラートが発生した際に、それが「モデルの汎化性能の問題」なのか否かを判断できるようにしました。

■ まとめ

今回の取り組みでは、推薦システムの KPI モニタリングにおける「アラートがモデルの性能に起因するのか実測値の異常によるのか」という課題に対して、Cross Validation の導入と評価指標(Coverage・MAPE)の活用を通じて、モデル性能の精度を定量的に評価する仕組みを導入しました!

この結果、異常値検知においてより適切な判断が可能となり、モニタリング体制の信頼性向上に繋がったかなと信じてます!引き続きシステムの安定性向上のために頑張って行きマウス🔥

株式会社ZOZO

Discussion