BigQuery MLでスロット使用量が急増しているプロジェクトやユーザーを異常検知する

背景

  • BigQueryはオンデマンドとフラットレート(定額料金)がある
    • オンデマンドはスキャン量がお金に直結するため、INFORMATION_SCHEMA.JOBS_BY_*などを使ってクエリ警察をしている方も多いはず
    • INFORMATION_SCHEMAに代表されるデータ管理に役に立つ現場のノウハウを最近会社のTech Blogに書いたので、そちらも見てね
  • 一方で、フラットレートに関しては定額使いたい放題のプランであるため、オンデマンドよりはクエリ警察をしていない場合もある
    • 見れるなら見たいが、どうしても支出に直結するオンデマンドを優先して見てしまいがち。工数も限られている
    • が、あまりに自由に使ってもらうと、社内の分析時の体験が悪くなってしまう...
      • 現状でのスロット使用量の推移はINFORMATION_SCHEMA.JOBS_TIMELINE_BY_*などを使って分析して、改善しましょう
  • 一時的に状況を改善したとしても、気が付くと徐々にスロット使用量がぐいぐい伸びてしまうGCPのプロジェクトやユーザーはまあ出てきてしまうもの
    • 毎回利用者用の分析ダッシュボードを人手で見るのは面倒だし、気付くのに時間がかかる
    • ここを自動化させることを試みたい
  • スロット使用量の文脈で説明しますが、ストレージコストでも大体同様の話ができますし、何なら広告出稿の単価が急上昇したとかそういうことにも使えるはず

どうやって異常を検知するか

  • 異常を検知するためのルールをチマチマ書くのは面倒だし、メンテナンスの手間もかかる
    • 増えた / 減ったといっても、ジワジワ増えていく場合もあるし急増する場合もある
    • 多いから一律にダメというわけでもなく、事前に多いことが分かっている場合(ホワイトリスト形式)もある
    • 土日は減ったり、月初は多くなるなどの周期性もある
    • 色々ダルい、機械学習で何とかして欲しい...
  • 機械学習システムのメンテナンスはそれはそれで大変
    • 前職で異常検知システムの開発運用もやっていたので、簡単ではないことは知ってる
    • 「効率よくdata managementをやりたい」というのが本来やりたかったことであり、機械学習のシステム構築は自分で頑張りたくない
    • AutoMLってやつに代わりに働いてもらおう
  • 今回はBigQuery MLを使ってみる
    • データの前処理、モデルの学習、評価、予測が全てSQLで完結する
    • 全部SQLで済むため、定期実行もスケジュールクエリにお任せで済む
      • 学習 / 予測用のGKEの用意とかそういうことを考えなくて済む
      • 運用コストを限りなくマネージドサービスに寄せることができる

BigQuery MLでの異常検知

検知できるモデルの種類

  • 時系列の要素を考えるモデルと考えないモデルがある
  • 時系列を考えないモデルとしてはK-meansクラスタリングとAutoencoderを使ったものがある
    • K-means: データ点と一番近いクラスタの代表点との距離が閾値を越えたものを異常値として扱う
    • Autoencoder: データを一旦低次元に落として、元のデータを再構築するNNモデルを考える。元データと再構築されたデータの誤差が閾値を越えたものを異常値として扱う
  • 時系列を考えるモデルとしてはARIMA_PLUSがある
    • ARIMAモデルは時系列予測の分野で古くから使われているモデル
      • 私も学部の計量経済学の授業で学びました
    • 以下のモデルの組み合わせ
      • 過去の自分自身のデータに重みを付けたモデル(ARモデル)
      • 過去のホワイトノイズ(誤差項)に重みを付けたモデル(MAモデル)
    • 組み合わせたモデルのことを自己回帰移動平均モデル(ARMAモデル)と呼ぶ
      • ARMAモデル自体は時系列データが定常なことを仮定しているが、現実のデータが定常性を満たすことは稀
      • そのため、 Y_t = X_t - X_{t-1}のように差分を取って定常にする(定常にならない場合はさらに差分を取って二階差分にするとか)ことが多く、この差分まで考えたARMAモデルのことをARIMAモデルと呼ぶ
    • Yule-Walker方程式を解いてモデルパラメータの推定したり、適切なモデルの次数をAICで決めるなどが本来は必要であるが、そのあたりはBigQuery MLがよしなにやってくれる
    • 時系列モデルでは「週末は平日より下がる」や「月初は普段より多い」といった周期性も考慮する必要がある(seasonal-ARIMAモデル)が、BigQuery MLはその辺も面倒を見てくれるので楽チン
  • 時系列を考慮したい場面は多いので、今回はARIMA_PLUSを使ってみる
    • BigQuery MLが色々よしなに前処理してくれる
      • 重複しているtimestampの削除
      • 欠損しているデータの補完
      • 季節性や祝日などの調整
      • 異常値のクリーニング
      • ML.EXPLAIN_FORECASTXAI的なことまでやってくれる

f:id:syou6162:20220307020752p:plain
ARIMA_PLUSのパイプライン

共通設定

以下、パラメータやテーブルを使い回すので、BigQuery Scriptingでの利用を想定して書いてます(が、そうでなくても使えます)。分割してSQLを書いていますが、動かす場合はmain.sqlにまとめて書いて、以下のようにパラメータを指定します。

cat main.sql | \
  bq query --nouse_legacy_sql \
    --parameter="model_project:STRING:my_project" \
    --parameter="model_dataset:STRING:my_dataset" \
    --parameter="model_table:STRING:my_model" \
    --parameter="split_date:STRING:2022-03-01" \
    --parameter="anomaly_prob_threshold:FLOAT64:0.99" \
    --parameter="anomaly_count_threshold:INT64:3"

各パラメータの意味は大まかにこんな感じです。

  • model_{project,dataset,table}: ARIMA_PLUSのBigQuery MLのモデルを保存する場所
  • split_date: 学習期間と予測期間を分割する日付
  • anomaly_prob_threshold: 異常と判定する際の確率の閾値
    • 累積確率を考えて、この閾値より大きいものを異常と判定する
  • anomaly_count_threshold: anomaly_prob_threshold以降の日でこの閾値より多い回数異常と判定されたプロジェクトを出力する

データの前準備

データの準備をします。INFORMATION_SCHEMA.JOBS_BY_ORGANIZATIONから日毎のスロットの使用量を集計します。通常、時系列モデルと言ったら単一の時系列(y_1, y_2, \cdots, y_T)を想定すると思いますが、ARIMA_PLUSはM本の複数の時系列(\left((y_{p_1, 1}, y_{p_1, 2}, \cdots, y_{p_1, T}), \cdots, (y_{p_M, 1}, y_{p_M, 2}, \cdots, y_{p_M, T})\right))を一個のモデルで扱うことができます。ここでy_{p_k, t}は時刻tでのk番目のプロジェクトのスロット使用量を表わします。

BigQuery MLの内部的に本当に一つのモデルとして取り扱っているのか(共通のpriorを持つ階層ベイズモデルっぽいものを想定)、1つの時系列に対してARIMAモデルを作ったものを1つのモデルとして見せているだけなのかは分からなかったです。

-- テーブル名は変数として渡せないので、EXECUTE IMMEDIATEでSQLを構築する
DECLARE model_project STRING DEFAULT @model_project;
DECLARE model_dataset STRING DEFAULT @model_dataset;
DECLARE model_table STRING DEFAULT @model_table;

-- 学習とテスト用のテーブルを作る
CREATE OR REPLACE TEMP TABLE information_schema_ts_test AS
WITH
  total_slot_by_date_and_project_id AS (
  SELECT
    DATE(creation_time) AS creation_date,
    project_id,
    total_slot_ms,
  FROM
    `region-us`.INFORMATION_SCHEMA.JOBS_BY_ORGANIZATION
  ),
  data_points_count_by_project AS (
    SELECT
      project_id,
      COUNT(DISTINCT(creation_date)) AS data_points_count,
      SUM(total_slot_ms) AS sum_total_slot_ms,
    FROM
      total_slot_by_date_and_project_id
    GROUP BY
      project_id
  )
SELECT
  creation_date,
  project_id,
  SUM(total_slot_ms) AS original_total_slot_ms,
  LOG10(SUM(total_slot_ms)) AS total_slot_ms,
FROM
  total_slot_by_date_and_project_id
WHERE
  project_id IN ( 
    SELECT
      project_id
    FROM
      data_points_count_by_project
    WHERE
      data_points_count > 100 -- データが十分にあるプロジェクトのみに絞る
    ORDER BY sum_total_slot_ms DESC -- スロット使用量が少ないプロジェクトの異常を検知しても仕方ないので、上位100プロジェクトに絞る
    LIMIT 100
  )
GROUP BY
  creation_date,
  project_id
ORDER BY
  project_id,
  creation_date;

SQL自体は簡単ですが、ポイントは以下です。

  • 適切なデータの絞り込み
    • プロジェクトによっては最近できたものも当然ある。データ点数が少ないプロジェクトは除外しておく(学習もまともに動かないであろう)
    • スロット消費量が少ないプロジェクトの異常を検知しても仕方ないので、学習も予測もスロット使用量が多いプロジェクトに絞る
  • データの対数化
    • 最初はスロット使用量の生データをBigQuery MLに食わせていたのですが、誤検知がかなり多かった
    • ARIMAモデルは誤差項の正規性を仮定しているが、スロット使用量はかなりskewな分布をしているため、対数を取るとモデルの仮定により近づき誤検知が少なくなった
      • AutoMLではあるが、この辺はある程度統計モデリングに慣れてないとBigQuery MLの真のパフォーマンスを出せないことがありそう...

モデルの学習

概ねBigQuery MLのAPIを叩いてるだけです、楽チン。BigQuery MLは以下の3つのカラムを取るデータしか受け取らないので、EXCEPTで不要なカラムを落とします。

  • TIME_SERIES_TIMESTAMP_COL: 時系列の日付やtimestampなどを表わすカラム
  • TIME_SERIES_DATA_COL: 学習したい数値データ。ここではプロジェクト毎の毎日のスロット数
  • TIME_SERIES_ID_COL: 時系列のID。ここではプロジェクトのID
EXECUTE IMMEDIATE FORMAT("""
  CREATE OR REPLACE MODEL `%s.%s.%s`
  OPTIONS(
    MODEL_TYPE='ARIMA_PLUS',
    TIME_SERIES_TIMESTAMP_COL='creation_date',
    TIME_SERIES_DATA_COL='total_slot_ms',
    TIME_SERIES_ID_COL='project_id',
    HOLIDAY_REGION='JAPAC' 
  ) AS
  SELECT
    * EXCEPT(original_total_slot_ms)
  FROM
    information_schema_ts_test
  WHERE
    creation_date < DATE('%s');
""", model_project, model_dataset, model_table, @split_date);

モデルを元にスロット使用量が異常に増加していないか予測する

学習したモデルでテスト期間のデータを予測し、異常があるプロジェクトの一覧を出します。出力結果を元にSlackで通知などは別途やればよいでしょう。

ARIMA_PLUSでは指定した閾値(anomaly_prob_threshold)に対して異常かどうかをis_anomalyのカラムで出してくれます。しかし、このカラムで異常を判定すると「スロット使用量が過去の傾向と異なり、異常に少ない」プロジェクトも異常となってしまいます。今回はスロット使用量が急増しているプロジェクトに興味があるので、このカラムを使うのは不適切です。ARIMA_PLUSでは、上限(upper_bound)と下限(lower_bound)も出してくれるので、今回は上限を越えているかどうかを判定に使うことにします。

また、一点だけ異常があるようなプロジェクトも出してしまうと誤検知が多かったため「(例えば)1週間分の予測をしてそのうち3日より多い日が異常だったら、そのプロジェクトを異常と判断する」という風にしています。この辺はMackerelの異常検知にも入ってる最大試行期間の考え方と一緒です。異常検知のアラートを運用に乗せるためには、false positiveをなるべく減らすのをまず先に考えるとよいですね。

CREATE OR REPLACE TEMP TABLE prediction_period_data AS
SELECT
  *
FROM
  information_schema_ts_test
WHERE
  creation_date >= DATE(@split_date);

EXECUTE IMMEDIATE FORMAT("""
CREATE OR REPLACE TEMP TABLE predictions AS
SELECT
  *,
  total_slot_ms > upper_bound AS anomaly,
  POWER(10, total_slot_ms) AS total_slot,
FROM
  ML.DETECT_ANOMALIES(
    MODEL `%s.%s.%s`,
    STRUCT(%f AS anomaly_prob_threshold),
    (SELECT * FROM prediction_period_data)
  );
""", model_project, model_dataset, model_table, @anomaly_prob_threshold);

WITH anomaly_count_by_project_id AS (
  SELECT 
   project_id,
   SUM(IF(anomaly IS TRUE, 1, 0)) AS anomaly_count
  FROM
    predictions
  GROUP BY 
    project_id
  ORDER BY anomaly_count DESC
)
SELECT
  predictions.*
FROM
  predictions
INNER JOIN
  anomaly_count_by_project_id
ON
  predictions.project_id = anomaly_count_by_project_id.project_id
WHERE
  anomaly_count > @anomaly_count_threshold
ORDER BY
  project_id,
  creation_date;

所感

  • かなり使い勝手がよい
    • 100行程度のSQLを書くだけで、前処理から学習、予測までBigQueryが面倒を見てくれるので気にする要素が少なくて助かる
      • scikit-learnでfit/predictするだけで(ある程度のモデルは)簡単に使える + それを実行するインフラのことを考えなくて済む、というのは機械学習が本業ではない人にとってはかなりうれしいのではないか
    • 機械学習が本業の人たちもモデルのプロビジョニングとかダルいことを考えずにさっとできるのはうれしいこと多いはず
  • 全てがSQLで完結する
    • これは結構うれしくて、SQLで完結すると定期的な学習や予測すらスケジュールクエリで実行できるということを意味します
    • 画面をぽちぽちやるだけでいいのは正直かなり楽
    • 同じことをGKEでやろうと思うと、まあまあダルい
      • エンジニア工数は不足気味なので、凝ったことはできないがSQLは書ける、というチームにはよさそう
  • 気になる料金は公式のページを見るのがよい
    • 機械学習なのでいつもと若干課金体系が異なる。監査ログから様子が分かるので、ちゃんと確認しましょう
    • 定額料金だとBigQuery MLも安心して使える