お客様のDXの推進やクラウド活用をサポートする
NRIグループのプロフェッショナルによるブログ記事を掲載

学習ゼロで時系列予測 ~ OCI Data Science で手軽にはじめるゼロショット予測 ~

20260511165354

はじめに

こんにちは、NRIでOracle Cloud Infrastructure(以下、OCI)の活用推進を担当している白濱です。

「来月の需要はどれくらいになりそうか」「来週の問い合わせ件数を見込んでおきたい」など、ビジネスの現場では、時系列データから先の動きを予測したい場面がたくさんあります。金融の取引量、小売の販売数、エネルギーの需給、設備の稼働、コールセンターの入電件数など、業種を問わず、未来の数値を見通すことは、在庫・人員配置・設備投資といった意思決定の質を大きく左右します。

近年、この時系列予測の世界に大きな変化が起きています。GPTに代表される大規模言語モデル(以下、LLM)で広がった事前学習済みのモデルをそのまま使うという流れが、時系列予測の領域にもやってきました。その一つが、IBMが公開している基盤モデルGranite Time SeriesのTiny Time Mixers(以下、Granite TTM)です。

本記事では、このGranite TTMをOCIのマネージドサービスであるOCI Data Science上で動かし、東京エリアの電力需要を題材にゼロショット予測を試してみます。追加学習なしでどこまで予測ができるのか、実際に動かしながら確かめてみます。

 

ゼロショット時系列予測とは

ゼロショットとは、対象データで一切の追加学習を行わずに、事前学習済みのモデルをそのまま使って予測することを指します。ChatGPTに質問を投げると、直接学習させていない内容であってもそれらしい回答が返ってくると思います。LLMの世界ではこの体験を通じて広く知られている考え方です。

時系列予測の領域でも、この発想を活かしたモデルが登場し始めており、Amazon、Google、IBM等から複数公開されています。Granite TTMはその代表例で、IBM ResearchがNeurIPS 2024で発表した基盤モデルです。世界中の多様な時系列データを使って事前学習されており、初めて見るデータに対しても、過去の値の流れを入力するだけで先の値を予測できます。

特徴を整理すると、次のようになります。

  • 追加学習が不要 : 対象データを与えるだけで予測ができる
  • 軽量 : モデルサイズは百万パラメータ程度で、LLM(数十億パラメータ以上)と比べて格段に小さい
  • 高速 : 推論は高速でノートPCのCPUでも動作する
  • 多変量対応 : 複数の系列を同時に扱い、相互の関係性も考慮できる

ビジネス的に重要なのは、予測モデルを作るというプロセスの形が変わりつつある、ということです。これまでは、対象業務のデータを集めて学習し、評価して、運用に乗せるというサイクルが必要でした。ゼロショット予測ではこの最初のサイクルを大きく短縮し、まず予測結果を見てから次の打ち手を考える、という進め方ができるようになります。

OCIに関するソリューション・事例はこちら

 

OCI Data Scienceで時系列予測を動かす

OCI Data Scienceは、機械学習モデルの開発から運用までを支えるOCIのマネージドサービスです。Notebook環境、モデルのデプロイ機能、GPUインスタンスがひと通り揃っており、Hugging Faceで公開されているオープンモデルを取り込んで動かすこともできます。

AI Quick Actionsという選択肢

OCI Data ScienceにはAI Quick Actionsという機能が用意されています。これは、よく使われる基盤モデルをほぼノーコードでデプロイ・利用できる仕組みで、時系列予測の領域でもGranite TTMが組み込みサポートされています。モデルカードからDeployボタンをクリックするだけで予測APIが立ち上がる手軽さです。

ただし、本記事執筆時点でAI Quick Actionsが対応しているのはGranite TTMのr1(初代リリース)のみです。Granite TTMはその後、対応する時系列粒度や予測精度が強化されたr2シリーズが公開されており、本記事で扱う毎時データの予測においてもr2のほうがより新しい設定が選べます。今回はr2を使うため、AI Quick Actionsではなく後述するモデルデプロイメントを利用しました。

モデルデプロイメントでモデルを立ち上げる

モデルデプロイメントは、任意のモデルをHTTPエンドポイントとして公開できるOCI Data Scienceの機能です。Hugging Faceから取得したモデルをオブジェクトストレージに置き、推論用のコンテナと組み合わせてデプロイする、という流れになります。

主な手順は次の通りです。

1.Notebookセッションでモデルを取得
OCI Data ScienceのNotebookを起動し、Hugging FaceからGranite TTMのr2モデルファイル一式をダウンロードします。

 

2.Object Storageにアップロードしモデルカタログに登録

ダウンロードしたモデル一式を、オブジェクトストレージバケットにアップロードします。続いてモデルカタログにモデルを登録すると、以降のモデルデプロイメントから参照できるようになります。

 

3.推論ロジックの準備

Granite TTMを動かす推論ロジック(score.py)と、依存ライブラリを定義する環境ファイル(runtime.yaml)を作成し、モデルファイルと一緒に1つの成果物としてまとめます。今回はOCI Data Scienceが提供する推論コンテナを利用したため、コンテナイメージのビルドやレジストリへのpushはせずに、簡単にモデルデプロイの準備ができました。

score.pyのサンプル

"""
score.py for OCI Data Science Model Deployment — TTM r2 (base, zero-shot).

Contract:
    load_model()          -- called once at container start
    predict(data, model)  -- called on every request
"""
Input payload (dict or JSON string):
    {
        "past_values":          [[[...], ...]],   # [batch, context_length, n_channels]
        "past_observed_mask":   [[[...], ...]],   # optional, same shape (0/1)
    }

Output:
    {"prediction": [[[...], ...]]}           # [batch, prediction_length, n_channels]

"""

from __future__ import annotations

import json
import logging
import os
import traceback
from typing import Any, Dict, Union

logger = logging.getLogger(__name__)
if not logger.handlers:
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s %(levelname)s %(name)s %(message)s",
    )

MODEL_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "model")

def load_model() -> Any:
    """Load the TinyTimeMixerForPrediction model once at startup."""
    import torch
    from tsfm_public.models.tinytimemixer import TinyTimeMixerForPrediction

    logger.info("Loading TinyTimeMixerForPrediction (r2 base) from %s", MODEL_DIR)
    model = TinyTimeMixerForPrediction.from_pretrained(MODEL_DIR)
    model.eval()
    if torch.cuda.is_available():
        model = model.to("cuda")
        logger.info("Moved model to CUDA")
    else:
        logger.info("Using CPU for inference")
    logger.info(
        "config: context_length=%d prediction_length=%d scaling=%s n_channels=%d",
        model.config.context_length,
        model.config.prediction_length,
        model.config.scaling,
        model.config.num_input_channels,
    )
    return model

def _coerce(data: Union[str, bytes, bytearray, Dict[str, Any]]) -> Dict[str, Any]:
    if isinstance(data, (bytes, bytearray)):
        data = data.decode("utf-8")
    if isinstance(data, str):
        data = json.loads(data)
    if not isinstance(data, dict):
        raise TypeError(f"Expected dict or JSON string, got {type(data).__name__}")
    return data

def predict(data: Union[str, Dict[str, Any]], model: Any = None) -> Dict[str, Any]:
    try:
        import torch

        if model is None:
            model = load_model()

        payload = _coerce(data)
        if "past_values" not in payload:
            raise KeyError("'past_values' is required in the request payload")

        device = next(model.parameters()).device
        pv = torch.tensor(payload["past_values"], dtype=torch.float32, device=device)
        if pv.dim() != 3:
            raise ValueError(
                f"'past_values' must be shape [batch, context, channels]; got {tuple(pv.shape)}"
            )

        mask = None
        if payload.get("past_observed_mask") is not None:
            mask = torch.tensor(
                payload["past_observed_mask"], dtype=torch.float32, device=device
            )
            if mask.shape != pv.shape:
                raise ValueError(
                    f"'past_observed_mask' shape {tuple(mask.shape)} "
                    f"!= 'past_values' shape {tuple(pv.shape)}"
                )

        with torch.no_grad():
            out = model(
                past_values=pv,
                past_observed_mask=mask,
                output_hidden_states=False,
                return_dict=True,
            )
        pred = out.prediction_outputs  # [B, pred_len, C]

        return {"prediction": pred.detach().cpu().tolist()}
    except Exception as exc:  # noqa: BLE001 — contract requires non-fatal
        logger.exception("predict() failed")
        return {"error": str(exc), "trace": traceback.format_exc()}


runtime.yamlのサンプル

MODEL_ARTIFACT_VERSION: '3.0'
MODEL_DEPLOYMENT:
  INFERENCE_CONDA_ENV:
    INFERENCE_ENV_SLUG: ttm_r2_inference_v1
    INFERENCE_ENV_TYPE: published
    INFERENCE_ENV_PATH: oci://[バケット名]@[ネームスペース名] /conda_environments/cpu/cpu/ttm_r2_inference/1/ttm_r2_inference_v1
    INFERENCE_PYTHON_VERSION: '3.9'

 

4.モデルデプロイメントの作成

コンソールからモデルデプロイメントを新規作成します。設定する主な項目は以下です。

- Model : ステップ2で登録したモデル。
- Computeシェイプ : 推論で使うインスタンスタイプ。今回はVM.Standard.E4.Flex(CPU 2 OCPU / 16GB)。
- Replica: 冗長化や負荷分散の単位。今回は検証用途なので1で設定。
- Logging : 推論ログとアクセスログを有効化(トラブルシューティング用)。


 

5.デプロイと推論エンドポイントの取得

設定後、デプロイを実行すると数分でモデルが稼働を始めます。完了すると推論用のRESTエンドポイントURLが払い出されるので、Notebookや任意のクライアントからPOSTリクエストを送って予測結果を得られます。



ここまでの作業は、慣れていれば30分程度で完了します。実際の検証では、モデルデプロイメントのデプロイ自体は6分ほどで終わり、推論エンドポイント経由での予測も100ミリ秒程度で結果が返る軽さでした。詳細な手順やIAMポリシーの設定方法はOCI公式ドキュメントをご参照ください。

 

お題 : 電力需要予測

モデルを動かす環境が整ったところで、いよいよ実際の予測に進みます。ゼロショット予測の実力を確かめる題材として、東京エリアの電力需要を選びました。電力需要は社会インフラデータの代表例であり、明確な周期性を持つ時系列データです。日中と夜間の差や季節ごとの変動など、直感的にデータの性質を理解しやすい題材のため、今回の検証で利用することにしました。

データ

東京電力パワーグリッド株式会社がでんき予報サイトで公開している過去実績データを利用しました。1時間ごとの電力使用実績がCSVで提供されています。

- 取得元 : 東京電力パワーグリッドでんき予報過去実績
- 粒度 : 1時間ごと
- 取得期間 : 2024年7月14日〜2024年8月8日(予測対象期間とそのコンテキスト)

出典:東京電力パワーグリッド株式会社「でんき予報」過去実績データ

 

検証設計

冷房需要が急増する真夏のピーク期を対象に、ゼロショット予測の挙動を確認します。

- 予測対象期間 : 2024年8月5日(月)00:00から96時間(4日間)
- 入力コンテキスト : 予測開始時点から過去512時間(21日間)の実績


Granite TTMのr2に用意されている設定の中から、コンテキスト長512・予測長96のモデルを採用しました。



予測結果は実測値と重ね合わせて可視化し、予測誤差を以下の3つの指標で評価します。

- MAPE(平均絶対パーセント誤差): 予測値が実測値から何%ずれているか
- MAE(平均絶対誤差): 予測値と実測値のずれを電力量(MW)で表した値
- RMSE(二乗平均平方根誤差): 大きな誤差をより強く反映した指標


予測結果

実際にGranite TTMをゼロショットで動かし、4日間(96時間)の予測を実行しました。結果は次の通りです。

予測 vs 実測の重ね書き

灰色の線が入力した過去21日間のコンテキスト(グラフでは直近約1週間分を表示)、青色の線がGranite TTMの予測、赤色の線が実際の電力需要です。予測開始時点を縦の点線で示しています。

予測誤差を3つの指標で評価したところ、次の値となりました。

指標
MAPE(平均絶対パーセント誤差) 4.36%
MAE(平均絶対誤差) 1,953MW
RMSE(二乗平均平方根誤差) 2,456MW

電力需要予測において求められる精度は非常に高いものですが、今回の検証では、活用を視野に入れられる良好な結果が得られました。グラフを見ても、日中のピークと夜間の底、そして平日と週末のリズムが丁寧にトレースされていることが分かります。

なお、推論にかかる時間もCPUを用いた推論にもかかわらず高速なレベルでした。実際に、モデルデプロイメントのエンドポイントへのリクエストから結果が返るまでが約100ミリ秒程度でした。検証用に用意したCPUインスタンス(VM.Standard.E4.Flex、2 OCPU)で十分動作しました。

 

検証から見えてきたこと

ゼロショットでも実用水準の精度

今回の検証で、事前学習済みモデルをそのまま使うだけで、これだけの精度が出るということが分かりました。電力需要は社会インフラの中でも特に変動の大きい時系列データですが、Granite TTMは過去21日間のデータを見ただけで、データの周期性を概ね再現できました。これはGranite TTMの事前学習データに、多様な時系列データが含まれているためで、電力需要のような周期性のあるパターンは初見でもある程度の予測ができるのだと考えられます。

ピーク時間帯では予測が控えめになる傾向

一方で、結果を細かく見ると、毎日の昼ピーク時間帯で系統的に3〜5GWほど過小予測する傾向がありました。

時間帯別MAE

時間帯別のMAEを見ると、深夜0〜6時は500〜800MW程度に収まる一方、13〜15時のピーク時間帯では4,000MW級まで誤差が大きくなります。夏の冷房需要は気温に強く依存するため、コンテキスト期間と予測対象期間で気温水準が異なると予測がずれやすくなる、というのが主な要因と考えられます。

これはゼロショット予測がピーク時に弱いというよりは、気温情報などの追加情報なしでは追いきれない部分と思われます。さらなる精度向上のヒントは、後ろで簡単に触れます。

試行錯誤のコストが大きく下がる

今回の検証は、データ取得・モデルデプロイ・予測実行・評価まで含めて、数時間ほどで完了しました。従来の予測モデル開発では、データ準備と特徴量設計だけで数日かかることも珍しくなかったことを考えると、検証のスピード感が大きく変わることを実感しました。これは、試行錯誤を繰り返しながら最適解を探すような実務シーンで、特に大きな価値を持つと思います。

 

さらに精度を上げるには

今回はGranite TTMをゼロショットでそのまま動かしましたが、対象データに合わせてさらに精度を高めることもできます。代表的な手法がファインチューニングです。

これは、対象データの一部を使って、事前学習済みモデルを軽く追加学習する手法です。業界や企業特有のパターンをモデルに反映できるため、ゼロショットでは捉えきれない細かな傾向も学習できます。Granite TTMは軽量なので、ファインチューニング自体も短い時間で可能です。

ファインチューニングを行うと、対象の予測値に影響を与える別の系列を補助情報として取り込むこと(外生変数の活用)も可能になります。電力需要に対する気温のように、業務に紐づく外部要因をモデルに反映できるため、現実の予測タスクではこのアプローチが有力な選択肢となります。

 

さいごに

いかがでしたでしょうか?

時系列予測は、ビジネスのあらゆる場面で必要とされる一方で、従来は専門的な準備や検討が必要な領域でもありました。今回ご紹介したGranite TTMのような基盤モデルとOCI Data Scienceを組み合わせれば、追加学習なしで、まず予測結果を見るところから始められるようになります。試してみることのハードルが大きく下がり、活用の選択肢が広がる流れだと感じています。

NRIでは、OCIをはじめとする各種クラウド上でのAI活用支援を幅広く手がけています。時系列予測に限らず、お客様のデータや業務に合わせた最適なアプローチをご提案いたしますので、お気軽にご相談ください。

 

atlax公式SNS

各種SNSでも情報を発信しています。ぜひフォローをお願いいたします。

 

     

 

お問い合わせ

atlax では、ソリューション・サービス全般に関するご相談やお問い合わせを承っております。

 

関連リンク・トピックス

・NRIクラウド OCI 区画 | NRIマルチクラウド | atlax (アトラックス) | 野村総合研究所 (NRI)

・IBM Granite Time Series Models(Hugging Face)※外部サイト

・OCI Data Science 公式ドキュメント ※外部サイト

 

NRIの キャリア採用

採用情報

NRIの IT基盤サービスでは、キャリア採用を実施しています。様々な職種で募集しておりますので、ご興味を持たれた方は キャリア採用ページも ぜひご覧ください。

・NRI / キャリア採用情報 / 職種一覧 ※NRIサイトへ

※ 記載された会社名 および ロゴ、製品名などは、該当する各社の登録商標または商標です。
※ アマゾン ウェブ サービス、Amazon Web Services、AWS および ロゴは、米国その他の諸国における、Amazon.com, Inc.またはその関連会社の商標です。
※ Microsoft、Azure は、米国 Microsoft Corporation の米国およびその他の国における登録商標または商標です。
※ Google Cloud、Looker、BigQuery および Chromebook は、Google LLC の商標です。
※ Oracle、Java、MySQL および NetSuite は、Oracle Corporation、その子会社および関連会社の米国およびその他の国における登録商標です。NetSuite は、クラウド・コンピューティングの新時代を切り開いたクラウド・カンパニーです。