サーバーワークスの村上です。
このブログでは、時系列予測モデルである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 |

XGBoost
| 指標 | 値 |
|---|---|
| RMSE | 64.78 |
| MAE | 48.02 |
| R2 | 0.92 |

ランニングコストでは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 (決定係数)
決定係数は回帰分析の当てはまりの良さを表す指標です。
計算式で表すと次のようになります。
式右側の分子は「予測値と正解値との誤差の二乗和」で、分母は「正解値と、その正解値の平均との誤差の二乗和」です。
正解値の平均だけで予測した場合の誤差と比較することで、「このモデルはどれくらいデータの変動を説明できているか」を 1 に近いほど良い、という形で評価します。
以上、今回は時系列予測モデルのChronosを紹介しました。