背景
- 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モデルは時系列予測の分野で古くから使われているモデル
- 私も学部の計量経済学の授業で学びました
- 以下のモデルの組み合わせ
- 組み合わせたモデルのことを自己回帰移動平均モデル(ARMAモデル)と呼ぶ
- ARMAモデル自体は時系列データが定常なことを仮定しているが、現実のデータが定常性を満たすことは稀
- そのため、のように差分を取って定常にする(定常にならない場合はさらに差分を取って二階差分にするとか)ことが多く、この差分まで考えたARMAモデルのことをARIMAモデルと呼ぶ
- Yule-Walker方程式を解いてモデルパラメータの推定したり、適切なモデルの次数をAICで決めるなどが本来は必要であるが、そのあたりはBigQuery MLがよしなにやってくれる
- 時系列モデルでは「週末は平日より下がる」や「月初は普段より多い」といった周期性も考慮する必要がある(seasonal-ARIMAモデル)が、BigQuery MLはその辺も面倒を見てくれるので楽チン
- ARIMAモデルは時系列予測の分野で古くから使われているモデル
- 時系列を考慮したい場面は多いので、今回は
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
から日毎のスロットの使用量を集計します。通常、時系列モデルと言ったら単一の時系列()を想定すると思いますが、ARIMA_PLUS
はM本の複数の時系列()を一個のモデルで扱うことができます。ここでは時刻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するだけで(ある程度のモデルは)簡単に使える + それを実行するインフラのことを考えなくて済む、というのは機械学習が本業ではない人にとってはかなりうれしいのではないか
- 機械学習が本業の人たちもモデルのプロビジョニングとかダルいことを考えずにさっとできるのはうれしいこと多いはず
- 100行程度のSQLを書くだけで、前処理から学習、予測までBigQueryが面倒を見てくれるので気にする要素が少なくて助かる
- 全てがSQLで完結する
- これは結構うれしくて、SQLで完結すると定期的な学習や予測すらスケジュールクエリで実行できるということを意味します
- 画面をぽちぽちやるだけでいいのは正直かなり楽
- 同じことをGKEでやろうと思うと、まあまあダルい
- エンジニア工数は不足気味なので、凝ったことはできないがSQLは書ける、というチームにはよさそう
- 気になる料金は公式のページを見るのがよい
- 機械学習なのでいつもと若干課金体系が異なる。監査ログから様子が分かるので、ちゃんと確認しましょう
- 定額料金だとBigQuery MLも安心して使える