売上や在庫などの時系列予測に使えるChronosモデルのご紹介

記事タイトルとURLをコピーする

サーバーワークスの村上です。

このブログでは、時系列予測モデルであるChronosと、参照用にXGBoostを使って推論してみた結果について記載します。

時系列予測のビジネス上のニーズ

時系列予測は単なるデータ分析の延長ではなく、意思決定の中枢を担う技術となっています。

例えば以下の3つのようなユースケースでビジネスインパクトをもたらします。

  • 在庫・リソースの最適化
  • 戦略的な意思決定(季節性のトレンドや市場の変動など)
  • リスク管理(製造機器の異常予兆検知など)

Chronosとは?

Amazonが開発する事前学習済みの時系列予測モデルです。オープンなモデルでGithubやHugging Faceで公開されています。

以下、公式ブログの説明を引用します。

これは大規模言語モデル(LLM)のアーキテクチャを活用して、これらの障壁を打破する最先端の時系列モデルファミリーです。Chronos は基盤モデルとして、大規模で様々なデータセットを使って事前学習を行っており、多くの分野で使える汎用的な予測能力を持っています。この革新的なアプローチにより、Chronos はゼロショット予測(対象データセットに特化した訓練なしで行う予測)において優れた性能を発揮します。

最初にまとめ

使用したデータセットはニューヨークのタクシー乗車数推移の時系列データ

NYC Taxi Trip Dataのデータセットを利用し、1か月間(2020-02)の乗車数を予測しました。

予測精度ではChronos優位

予測精度だけを比較するとChronosの方がXGBoostよりも結果が良かったです。

Chronos

指標
RMSE 58.55
MAE 39.23
R2 0.94

Chronosの結果(青線が正解ラベル、オレンジ色の線が予測値)

XGBoost

指標
RMSE 64.78
MAE 48.02
R2 0.92

XGBoostの結果(青線が正解ラベル、オレンジ色の線が予測値)

ランニングコストではXGBoost優位

ChronosはCPU/GPUに対応しており、ユースケースによって選択するインスタンスタイプは変わるかと思いますが、今回の検証ではHugging Faceの例にあるとおりml.g5.2xlargeを利用しました。

これに対して、今回XGBoostはml.t2.mediumにホストしましたので、ランニングコストではXGBoostが優位かと考えます。

冒頭に記載のとおり、Chronosは大規模に事前学習されたモデル(1億2000万パラメータをもつ)です。ユーザー側でモデルのトレーニングは必ずしも必要ではありません。しかし、今回XGBoostのトレーニング料金(ml.m5.largeで15分程度)を加味しても、この結論は変わらないでしょう。

補足

ChronosとXGBoostは異なるモデルであり、モデルのトレーニングや推論の方法が異なります。その意味で厳密な比較とは言えず、あくまで「今回の検証ではこのような結果だった」という前提での記載とご理解ください。以下、補足です。

モデルのトレーニングについて

Chronosは必ずしもトレーニングが必要ではなく、本検証でもモデルのトレーニングはせず、ゼロショットで推論しています。

ただし、推論パラメータpast_covariatesに過去の時系列データを渡す仕様になっています。

これに対し、XGBoostは2019年7月から12月までのの6か月間を学習データに、2020-01の1か月間を検証データにしてトレーニングしています(Optunaでハイパーパラメータ最適化も実施)。

推論の違い

Chronosは1度の推論で推論対象である2020年2月の1時間ごとのタクシー乗車数を一気に推論しています(マルチステップ。今回は24時間*28日間 = 672ステップ)。

あくまで推論時はpast_covariatesとして渡した2019年12月と2020年1月の情報と自身が推論した結果しか参照できません。例えば2020年2月20日の乗車数を予測するときに、1日前の2月19日の実績乗車数などを参照することは出来ません。

これに対し、XGBoostはループ処理で1時間ずつ予測を行っています(2020年2月1日0時の乗車数を予測→同日1時の乗車数を予測→...)。

また、Chronosとは異なり、例えば2月20日の推論を行うとき、ラグ特徴量である2月19日の実績の乗車数や走行距離などを基に推論することができます。直前の正解データを利用できるXGBoostの方が有利な条件のように思いましたが、それでもChronosの方が精度が高かった点は興味深い点でした。

実施した特徴量エンジニアリング(Chronos / XGBoost共通)

次のような特徴量エンジニアリングを実施しました。

まずはこちらのBlackbletと同様のサンプリングを実施しました。

ソースコードはこちらで公開されており、まとめると次のとおりです。

  • 各月のデータから20%をサンプリング
  • 15分ごとに乗車数を合計し、pickup_countという目的変数を作成
  • 次のラグ特徴量を作成
    • history_<xx>slots: 過去の乗車数を表す
      • 例えばhistory_12slotsなら3時間前(15分x12)の乗⾞数
    • passenger_count_mean_<xx>slot: 過去の乗客数の平均を表す
      • 例えばpassenger_count_mean_12slotsなら3時間前(15分x12)の乗客数平均
    • trip_distance_mean_<xx>slot: 過去の乗車距離の平均を表す
      • 例えばtrip_distance_mean_12slotsなら3時間前(15分x12)の乗⾞距離平均
    • fare_amount_mean_<xx>slot: 過去の乗車料金の平均を表す
      • 例えばfare_amount_mean_12slotsなら3時間前(15分x12)の乗⾞料金平均
    • extra_mean_<xx>slot: 過去の追加料金の平均を表す
      • 例えばextra_mean_12slotsなら3時間前(15分x12)の追加料金平均
    • tip_amount_mean_<xx>slot: 過去のチップ額の平均を表す
      • 例えばtip_amount_mean_12slotsなら3時間前(15分x12)のチップ額平均
    • tolls_amount_mean_<xx>slot: 過去の有料道路料金の平均を表す
      • 例えばtolls_amount_mean_12slotsなら3時間前(15分x12)の有料道路料金平均

Chronosでは時系列全体を推論エンドポイントに送るため、このままだとTorchServe のリクエストサイズ上限(デフォルト約 6.5MB)に抵触してしまいました。そのため特徴量を下記25種類に限定しました。

['pickup_count', 'time_slot', 'history_12slots', 'history_24slots', 'history_48slots', 'history_96slots', 'history_192slots', 'passenger_count_mean_12slot', 'passenger_count_mean_96slot', 'passenger_count_mean_192slot', 'trip_distance_mean_12slot', 'trip_distance_mean_96slot', 'trip_distance_mean_192slot', 'fare_amount_mean_12slot', 'fare_amount_mean_96slot', 'fare_amount_mean_192slot', 'extra_mean_12slot', 'extra_mean_96slot', 'extra_mean_192slot', 'tip_amount_mean_12slot', 'tip_amount_mean_96slot', 'tip_amount_mean_192slot', 'tolls_amount_mean_12slot', 'tolls_amount_mean_96slot', 'tolls_amount_mean_192slot']
all_cols = list(df_features.columns)

keep_cols = []

# 1. 必須カラム
for c in ["pickup_count", "time_slot"]:
    if c in all_cols:
        keep_cols.append(c)

# 2. history_*slots
history_keep_windows = [12, 24, 48, 96, 192]
for w in history_keep_windows:
    col = f"history_{w}slots"
    if col in all_cols:
        keep_cols.append(col)

# 3. *_mean_*slot
mean_keep_windows = [12, 96]   
mean_maybe_windows = [192]     
for c in all_cols:
    if "_mean_" in c and c.endswith("slot"):
        if any(f"_{w}slot" in c for w in mean_keep_windows + mean_maybe_windows):
            keep_cols.append(c)

df_features = df_features[keep_cols].copy()

また、Chronos-2 は1度の推論で最大1,024ステップまで推論できる仕様です(Max. Prediction Length = 1024)。

1か月分を推論させたかったため、1日あたりのprediction_lengthを33以下(1024 ÷ 31 ≒ 33)の推論に抑える必要があります。そこで15分ごとの乗車推移データではなく、1時間ごとの乗車推移データとなるよう、time_slot列の値が4の倍数である行以外は削除しました。このようにすることで、1日あたりのprediction_lengthは24になります。

df_features = df_features[df_features["time_slot"] % 4 == 0].copy()

※なお、「1時間ごと」と記載しましたが、今回はpickup_countを1時間分の合計に再集計していません。単純に行を削除しただけです。

モデルデプロイ

ChronosはSageMaker JumpStartからも利用可能です。今回は非同期推論エンドポイントを作成しました。

from sagemaker.jumpstart.model import JumpStartModel
 
model = JumpStartModel(model_id="pytorch-forecasting-chronos-2")
 
predictor = model.deploy(
    endpoint_name="pytorch-forecasting-chronos-2-async",
    initial_instance_count=1,
    instance_type="ml.g5.2xlarge",
    async_inference_config=async_config,
)

推論

JSON形式で推論リクエストを送るため、次のような構造のJSONを作成し推論しています。

  • target: 観測された時系列データ(ここでは2019年12月1日からのタクシー乗車数)
  • start: 最初の時系列データのタイムスタンプ
  • past_covariates: 時系列データの特徴量
  • prediction_length: 予測する必要がある将来の時系列(ここではfreqが1時間なので、672時間分(24時間 * 28日間)を予測するということ)
{
    "inputs": [
        {
            "target": [
                405,
                356,
                <中略>
                651
            ],
            "item_id": "nyc_taxi",
            "start": "2019-12-01T00:00:00",
            "past_covariates": {
                "time_slot": [
                    0,
                    4,
                    <中略>
                    92
                ],
                "history_12slots": [
                    487.0,
                    <中略>
                    660.0
                ],
                "history_24slots": [
                    572.0,
                    <中略>
                    669.0
                ],
                <中略>
            },
        }
    ],
    "parameters": {
        "prediction_length": 672,
        "freq": "h"
    }
}

結果の補足

RMSE(平均平方二乗誤差)

代表的な指標で、各レコードの目的変数の真の値と予測値の差の二乗をとり、それらを平均したあとに平方根をとる指標です。

値が小さいほど、予測が実測に近い=精度が良いことを意味します。また、差を二乗しているため、大きな誤差(外れ値)に敏感という特徴があります。

MAE(平均絶対値誤差)

真の値と予測値の差の絶対値の平均によって計算される指標です。RMSEに比べて外れ値の影響を受けにくいという特徴があります。

小さいほど精度が良いことを意味します。

R2 (決定係数)

決定係数は回帰分析の当てはまりの良さを表す指標です。

計算式で表すと次のようになります。

 \displaystyle R^2 = 1 - \frac{\sum_{i=1}^{n} (y_i - \hat{y}_i)^2}{\sum_{i=1}^{n} (y_i - \bar{y})^2}

式右側の分子は「予測値と正解値との誤差の二乗和」で、分母は「正解値と、その正解値の平均との誤差の二乗和」です。

正解値の平均だけで予測した場合の誤差と比較することで、「このモデルはどれくらいデータの変動を説明できているか」を 1 に近いほど良い、という形で評価します。

以上、今回は時系列予測モデルのChronosを紹介しました。

村上博哉 (執筆記事の一覧)

2020年4月入社。機械学習が好きです。