チュートリアル『BigQuery で数百万の時系列を使用してスケーラブルな予測を行う』をやってみた

今回のエントリーは『機械学習チームアドベントカレンダー2022』の2日目の記事です。 BigQuery MLにおいてたくさんのデータを効率的に学習する方法について学びました!
2022.12.02

皆さん、こんにちは。 クルトンです。

『機械学習チームアドベントカレンダー2022』に参加しており、今回のエントリーは『機械学習チームアドベントカレンダー2022』の2日目の記事になります。

前日(12/01)の記事はこちらです。是非ご覧ください。

今回、Google CloudのチュートリアルでBigQuery MLをしてみました。

STEP1~10に分かれており、予測と評価を繰り返しているチュートリアルになります。最後のSTEP10では、STEP9までで学んだ内容を使って100万の時系列データを予測しています。

今回はSTEP9までを実施して、100万の時系列データ予測をする際に知っておくと良い事柄を学びます。

データセットの作成とSQLの実行方法

チュートリアルの[BigQuery] ページに移動となっている青いボタンをクリックします。 自身のプロジェクト名のの部分をクリックして「データセットを作成」をクリックします。

create dataset picture

その後、チュートリアルにあるように[データセット ID] にbqml_tutorialを、[データのロケーション]にus (米国の複数のリージョン)を選択します。後で実行する機械学習モデルを学習させた際にはbqml_tutorialに作られるため、チュートリアル後に必要なくなったものを消す際に便利です。

次にSQLの実行方法です。SQLを入力する場合は、エディタと書いているタブへSQL文を書き、実行というボタンを押します。 本記事上で実行しているSQLは全て同様に実行します。他のタブを開きたい場合は、画像のエディタと書いている右隣の+ボタンを押すと新しいタブが開きます。 do sql editor

データの中身を確認

機械学習を行なう上で重要な事柄の1つが、データについて知る事です。

今回使用するデータセットは、citi bikeという自転車のシェアサービスで、複数箇所あるシティバイクステーションと呼ばれる自転車が設置されている場所から短期間レンタルして好きなシティバイクステーションへ返す事が出来るサービスのものです。

今回使用するデータセットについては、詳しくはこちらをご確認ください。

実際にデータの中身を確認してみます。

SELECT
  *
FROM
  `bigquery-public-data`.new_york.citibike_trips

このSQLの実行には30秒掛かりました。

カラムの一覧を上げると次のようになっています。

  • tripduration
  • starttime
  • stoptime
  • start_station_id
  • start_station_name
  • start_station_latitude
  • start_station_longitude
  • end_station_id
  • end_station_name
  • end_station_latitude
  • end_station_longitude
  • bikeid
  • usertype
  • birth_year
  • gender

ニューヨーク市で自転車を借りられるシティバイクステーションの名前や日付、どのシティバイクステーションで自転車を返したのかが表示されます。

また、tripdurationでは秒単位でどれだけ自転車を使っていたかが表示されています。(starttimeとstoptimeから計算も可能で、自転車を使って、どのシティバイクステーションからどのシティバイクステーションまでどれくらいの時間を掛けて移動したのかが確認できます。)

新たにテーブルを作成

時系列データをモデルの学習に使えるよう予測対象を含んで作成しています。

CREATE OR REPLACE TABLE
  bqml_tutorial.nyc_citibike_time_series AS
WITH input_time_series AS
(
  SELECT
    start_station_name,
    EXTRACT(DATE FROM starttime) AS date,
    COUNT(*) AS num_trips
  FROM
    `bigquery-public-data`.new_york.citibike_trips
  GROUP BY
    start_station_name, date
)
SELECT table_1.*
FROM input_time_series AS table_1
INNER JOIN (
  SELECT start_station_name,  COUNT(*) AS num_points
  FROM input_time_series
  GROUP BY start_station_name) table_2
ON
  table_1.start_station_name = table_2.start_station_name
WHERE
  num_points > 400

ここでやっているのは以下のような内容です。

  • bqml_tutorialデータセットにnyc_citibike_time_seriesというテーブルを作成して保存
  • WITH句内で実行したSQL文の結果にinput_time_seriesと名付け
    • 出発したシティバイクステーションを表示
    • 日時データをEXTRACT関数を使ってdateというカラム名をつけて表示
    • num_tripsというカラム名で行数(各シティバイクステーション毎のその日の利用された自転車数)を表示
  • WITH句で名前をつけたinput_time_seriesをtable_1という名前をつけてテーブルの中身を全部表示
    • INNER JOIN句でtable_1(input_time_seriesのこと)とtable_2として発行したSQL文の二つのテーブルの中で、出発したシティバイクステーションが同じものでnum_points(シティバイク毎の自転車が利用された総回数)が401以上になるものを合わせて表示

翻訳すると「自転車総利用回数をnum_pointsとして抽出し、シティバイクステーションの中でよく自転車が利用される(総利用回数401回以上の)ものを判定、日付(date)や出発した場所(start_station_name)、各シティバイクステーションの年月日で分けた時の利用回数(num_trips)を表示するテーブルを作成してnyc_citibike_time_seriesというテーブル名で保存」となります。

注意点として、GROUP BY句を使用して、シティバイクステーションごとに時系列データを分割して保存している所です。

実際に次のSQL文でテーブルbqml_tutorial.nyc_citibike_time_seriesに保存されている内容を確認してみます。

SELECT
  *
FROM
  bqml_tutorial.nyc_citibike_time_series

実行してみると、次の画像のような結果が返ってきます。 display nyc_citibike_time_series table

シティバイクステーション毎に、日付とその日の利用回数が表示されている事が確認出来ました。

以降、テーブルbqml_tutorial.nyc_citibike_time_seriesのデータを使ってモデルを作成していきます。

EXTRACT関数、WITH句、INNER JOIN句の詳しい使い方については以下Google公式サイトのドキュメントをご覧ください。

デフォルトのパラメータを使ったモデルの作成

先ほど作成したテーブルbqml_tutorial.nyc_citibike_time_seriesの、2016-06-01よりも前の情報を使って、モデルを学習させます。 学習時には、複数の時系列データを同時に予測するためにtime_series_id_colstart_station_nameを指定します。 (つまり今回の場合、自転車を利用開始したシティバイクステーションごとの時系列データを学習対象として設定する、となります。)

time_series_id_colではstring型かstring型の配列を渡します。

CREATE OR REPLACE MODEL bqml_tutorial.nyc_citibike_arima_model_default
OPTIONS
  (model_type = 'ARIMA_PLUS',
   time_series_timestamp_col = 'date',
   time_series_data_col = 'num_trips',
   time_series_id_col = 'start_station_name'
  ) AS
SELECT *
FROM bqml_tutorial.nyc_citibike_time_series
WHERE date < '2016-06-01'

自分の環境では学習完了までに11分7秒掛かりました。

デフォルトのパラメータを使ったモデルの予測精度を評価

作成したモデルの予測精度を評価します。

SELECT *
FROM
  ML.EVALUATE(MODEL bqml_tutorial.nyc_citibike_arima_model_default,
              TABLE bqml_tutorial.nyc_citibike_time_series,
              STRUCT(7 AS horizon, TRUE AS perform_aggregation))

今回の場合、STRUCT句に7という数値がある事とperform_aggregationがTRUEなので「データ7個先分まで予測した時の指標の数値データを表示する」するという事になります。これをFALSEにすると、シティバイクステーション毎の時系列データでなく、日付ごとに時系列データの予測精度を評価する事になります。

perform_aggregationがTRUEの時は次のような結果になります。

[{
  "start_station_name": "1 Ave \u0026 E 15 St",
  "mean_absolute_error": "37.716840317373787",
  "mean_squared_error": "2247.2839617198151",
  "root_mean_squared_error": "47.405526700162454",
  "mean_absolute_percentage_error": "18.215767896824772",
  "symmetric_mean_absolute_percentage_error": "19.865922454006906"
}, {
  "start_station_name": "1 Ave \u0026 E 18 St",
  "mean_absolute_error": "19.367628631338889",
  "mean_squared_error": "528.92829327539459",
  "root_mean_squared_error": "22.998441105331349",
  "mean_absolute_percentage_error": "14.182207445023336",
  "symmetric_mean_absolute_percentage_error": "14.617234800759412"
}, ...

対して、perform_aggregationをFALSEにすると次のような結果になります。

[{
  "start_station_name": "1 Ave \u0026 E 15 St",
  "date": "2015-09-26 00:00:00.000000 UTC",
  "num_trips": "257.0",
  "forecasted_num_trips": null,
  "lower_bound": null,
  "upper_bound": null,
  "absolute_error": null,
  "absolute_percentage_error": null
}, {
  "start_station_name": "1 Ave \u0026 E 15 St",
  "date": "2014-10-24 00:00:00.000000 UTC",
  "num_trips": "257.0",
  "forecasted_num_trips": null,
  "lower_bound": null,
  "upper_bound": null,
  "absolute_error": null,
  "absolute_percentage_error": null
}, ...

forecasted_num_tripsという部分を見ていただくと分かるのですが、シティバイクステーション毎でなく、日付毎の時系列データとしてnum_tripsを予測する事になります。今回予測時に使用しているテーブルのデータはシティバイクステーション毎に分割した時系列データを格納しているため、正しく予測が行なわれないので注意が必要です。

すべての時系列を包括的に予測精度を評価

SELECT
  AVG(mean_absolute_percentage_error) AS MAPE,
  AVG(symmetric_mean_absolute_percentage_error) AS sMAPE
FROM
  ML.EVALUATE(MODEL bqml_tutorial.nyc_citibike_arima_model_default,
              TABLE bqml_tutorial.nyc_citibike_time_series,
              STRUCT(7 AS horizon, TRUE AS perform_aggregation))

mean_absolute_percentage_errorsymmetric_mean_absolute_percentage_errorをAVG関数へ入れています。

各シティバイクステーションのデータ7個分の予測から平均を出しています。MAPEとsMAPEは異なる時系列のデータ複数を時系列予測した際に、モデルの精度を見るのに使われます。

例えば、異なる時系列データで大きさ(スケール)が違う場合でもパーセント表示で確認する事で、同様に予測精度を確認する事が出来ます。数値の算出に予測値と実測値の誤差の絶対値を用いているので、小さければ小さいほど正確に予測できていた事を表します。

MAPEが34.717431142740686で、sMAPEが25.631266881986171でした。

ハイパーパラメータの値を小さくしてモデルを学習

先ほど作ったモデルではauto_arima_max_orderがデフォルト値5を使用しています。 数値を2にする事で学習時のハイパーパラメータの探索空間が小さくなるため、学習に掛かる時間が削減されます。

CREATE OR REPLACE MODEL bqml_tutorial.nyc_citibike_arima_model_max_order_2
OPTIONS
  (model_type = 'ARIMA_PLUS',
   time_series_timestamp_col = 'date',
   time_series_data_col = 'num_trips',
   time_series_id_col = 'start_station_name',
   auto_arima_max_order = 2
  ) AS
SELECT *
FROM bqml_tutorial.nyc_citibike_time_series
WHERE date < '2016-06-01'

学習に掛かった時間は1分33秒でした。先ほど学習した時よりもかなり短い時間で学習が完了しています。

注意点として、学習範囲(ハイパーパラメータの探索空間)を狭めた結果、予測精度が下がる可能性があります。今作成したモデルを、デフォルト値で作成したモデルと同様に予測精度の確認をしてみます。

ハイパーパラメータの値を小さくして学習したモデルの予測精度を確認

nyc_citibike_arima_model_max_order_2モデルのMAPEとsMAPEを出力します。 ML.EVALUATEの引数であるMODELの名前を間違わないように注意してください。それ以外の箇所の記述は同じです。

SELECT
  AVG(mean_absolute_percentage_error) AS MAPE,
  AVG(symmetric_mean_absolute_percentage_error) AS sMAPE
FROM
  ML.EVALUATE(MODEL bqml_tutorial.nyc_citibike_arima_model_max_order_2,
              TABLE bqml_tutorial.nyc_citibike_time_series,
              STRUCT(7 AS horizon, TRUE AS perform_aggregation))

MAPEが33.372237227858811で、sMAPEが23.375205797395054でした。

先ほどよりも数値が小さいので、より上手く予測出来ている事が確認出来ました。チュートリアル上では次のように理由を考察しています。

この場合はハイパーパラメータ検索空間が小さいほど、予測精度が高くなります。理由の 1 つは、auto.ARIMA アルゴリズムが、モデリング パイプライン全体のトレンド モジュールに対してのみハイパーパラメータ調整を実行したためです。auto.ARIMA アルゴリズムによって選択された最良の ARIMA モデルが、パイプライン全体に対して最良の予測結果を生成するとは限りません。

単に学習時間を掛ければ良いというものでも無く、モデル作成時にはハイパーパラメータに関係する工夫が必要という事ですね。

ハイパーパラメータの値を小さくし学習に使うデータを制限して高速にモデル学習

これまでと違う部分はOPTIONS句のmax_time_series_lengthが追加されている所です。このオプションで数値を指定する事で学習に使われるデータを制御する事ができます。(データの最後から数値分のデータ個数だけ学習に使うよう指定する数値。)

CREATE OR REPLACE MODEL bqml_tutorial.nyc_citibike_arima_model_max_order_2_fast_training
OPTIONS
  (model_type = 'ARIMA_PLUS',
   time_series_timestamp_col = 'date',
   time_series_data_col = 'num_trips',
   time_series_id_col = 'start_station_name',
   auto_arima_max_order = 2,
   max_time_series_length = 30
  ) AS
SELECT *
FROM bqml_tutorial.nyc_citibike_time_series
WHERE date < '2016-06-01'

学習には39秒掛かりました。

max_time_series_lengthを加えるとさらに学習時間が短くなっています。ただし、予測精度がとても悪い場合は、実用的ではないです。したがって予測精度を確認します。

auto_arima_max_orderやmax_time_series_lengthの説明についてはこちらをご参照ください。

高速に学習したモデルの予測精度を評価

先ほど作ったモデルの名前はnyc_citibike_arima_model_max_order_2_fast_trainingなので、名前を間違わないようにお気をつけください。

SELECT
  AVG(mean_absolute_percentage_error) AS MAPE,
  AVG(symmetric_mean_absolute_percentage_error) AS sMAPE
FROM
  ML.EVALUATE(MODEL bqml_tutorial.nyc_citibike_arima_model_max_order_2_fast_training,
              TABLE bqml_tutorial.nyc_citibike_time_series,
              STRUCT(7 AS horizon, TRUE AS perform_aggregation))

MAPEが35.156290494403187で、sMAPEが24.730475626344216でした。

最初に学習したモデルnyc_citibike_arima_model_defaultよりも学習時間が大幅に減少し、予測精度も大きく変わらないので活用できそうです。

ここまでで、モデル学習に際してOPTIONS句で引数を上手く設定する事で、学習時間を大幅に減らしつつ予測精度が保たれている事を確認できました。

STEP10ではこれら高速に学習する手法を用いて、100万の時系列データをモデル学習に活用している例を実行できます。

実行環境削除

ここまで、チュートリアルの内容を実行してきました。チュートリアル後も使用する場合を除きますが、データを削除しましょう。

方法は簡単で、自身のデータセット名のの部分をクリックして「削除」をクリックします。 その後、「データセットを削除しますか?」とモーダルウィンドウが出てくるのでdeleteと入力して青い「削除」のボタンをクリックします。 modal window delet option

プロジェクトごと削除をしたい場合はチュートリアルに注意事項も含めて記載されているので、そちらをご覧ください。

終わりに

今回はBigQuery MLのチュートリアルを実行してきました。

時系列データ1つだけを予測していた前回触ったチュートリアルよりも実践的な内容となっており、モデルのOPTIONSに渡す引数によってモデル学習に関係する設定が調整出来る事など、学びがありました。

今回はここまで。

それでは、また!

参考にしたサイト