TimesFM 2.5 实战:端到端时序预测全流程

AI教程22小时前更新 程序员阿超
412 0 0

一、背景:为什么需要时序基础模型

传统时序预测长期处于”一数据一模型”的作坊模式:做零售销量用 LightGBM,做电力负荷用 DeepAR,做流量预测又换成 PatchTST。每个场景都要重新做特征工程、调参、训练,周期以周计算。更痛苦的是冷启动问题——新开一家门店、一个新 SKU 上线,历史数据只有两三周,传统模型根本训不起来,只能靠人工拍脑袋。

2024 年起,时序基础模型(Time Series Foundation Model)改变了这个局面。Google Research 开源的 TimesFM 把”预训练 + 零样本预测”的范式从 NLP 搬到了时序领域:先在海量公开时序语料(电力、交通、气象、零售、金融)上预训练一个 Transformer,然后对新序列直接做零样本推理,不需要任何微调。TimesFM 2.5 是该系列的最新版本,相比 2.0 有三个关键升级:第一,支持最长 2048 点上下文和最长 512 点前视预测,适合长周期零售补货场景;第二,原生输出分位数概率预测(默认输出 10%、50%、90% 等多条分位线),直接给出预测区间;第三,引入协变量回归接口 XReg,允许把促销、价格、节假日等外部变量喂给模型做条件预测。

本文带你走完一条完整的端到端链路:用一份仿真多门店零售数据,覆盖环境搭建、零样本点预测、分位带解读、多指标评估、批量推理、滚动回测、上下文长度消融、协变量融合、异常检测、长周期预测与吞吐调优。全部代码本地可跑,无需 GPU。

二、原理:TimesFM 2.5 是怎么预测的

2.1 分块自回归架构

TimesFM 把连续时序切成固定长度的 patch(例如每 32 个点一块),每块经线性投影变成一个 token,再送入 decoder-only Transformer。与语言模型逐词生成一样,模型逐块生成未来:给定历史 patch 序列,输出下一个 patch 的分布,反复滚动即可得到任意长度的前视预测。这种设计让模型长度泛化能力很强,训练时见过短序列,推理时也能处理长上下文。

2.2 点预测与分位预测

点预测输出的是中位数(q=0.5)路径,一般直接当作”最可能销量”。分位预测输出的是一族分位线:q10 下界、q50 中线、q90 上界,上下界之间的带宽就是预测不确定性。带宽窄说明模型对未来很确定(比如平稳周),带宽随前视步长发散说明不确定性累积(比如大促周)。评估分位质量用 pinball loss(分位损失)和区间覆盖率(PICP):理想情况下 80% 区间应该恰好覆盖约 80% 的真实值,覆盖过高说明区间太宽、过于保守,覆盖过低说明模型过度自信。

2.3 回测与协变量

单次 holdout 评估很容易”看运气”:恰好切在平稳段就显得很准,切在大促段就显得很差。滚动回测(rolling-origin backtest)在多个历史切点重复”用过去预测未来”,得到误差分布,才能判断模型是否稳定。协变量则解决”只看历史销量不够”的问题:价格降了 20%、门店做买一送一、下周是春节,这些信息不在历史销量曲线里,必须作为外部回归量显式输入。TimesFM 2.5 的 XReg 接口接受与目标序列对齐的协变量矩阵,模型内部做条件融合,相当于”历史规律 + 已知未来事件”联合预测。

三、环境准备

只需要 Python 3.10+,CPU 即可运行,内存建议 8GB 以上。安装官方包与科学计算栈:

pip install timesfm torch numpy pandas matplotlib scikit-learn -q
python -c "import timesfm; print(timesfm.__version__)"

模型权重首次加载时会自动从 HuggingFace 下载(约数百 MB),请保证网络通畅;内网环境可提前下载后用本地路径加载。随机种子固定保证可复现:

import random, numpy as np, torch
SEED = 42
random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)

加载模型与默认预测配置:

import timesfm

tfm = timesfm.TimesFm(
    hparams=timesfm.TimesFmHparams(
        backend="cpu",
        per_core_batch_size=32,
        horizon_len=28,          # 一次预测未来 28 天
        num_layers=50,
        model_dims=1280,
        use_positional_embedding=False,
    ),
    checkpoint=timesfm.get_checkpoint(repo_id="google/timesfm-2.5-200m-pytorch"),
)
print("模型加载完成")

封装一个可复用的配置切换函数,后续做消融实验时直接调用:

def make_forecaster(horizon=28, quantiles=(0.1, 0.5, 0.9), batch=32):
    return timesfm.TimesFm(
        hparams=timesfm.TimesFmHparams(
            backend="cpu", per_core_batch_size=batch,
            horizon_len=horizon, num_layers=50,
            model_dims=1280, use_positional_embedding=False,
        ),
        checkpoint=timesfm.get_checkpoint(repo_id="google/timesfm-2.5-200m-pytorch"),
    )

四、分步实战

步骤 1:生成仿真多门店零售数据

真实零售数据包含趋势、双季节性(周内高低峰、年度大促季)、价格弹性、促销脉冲、节假日、气温效应和噪声。下面代码生成 3 家门店、2 年日粒度销量,开箱即用:

import pandas as pd
import numpy as np

def gen_store(seed, base, trend, promo_lift, days=730):
    rng = np.random.default_rng(seed)
    t = np.arange(days)
    dates = pd.date_range("2023-01-01", periods=days, freq="D")
    weekly = 18 * np.sin(2 * np.pi * t / 7)            # 周季节性
    yearly = 30 * np.sin(2 * np.pi * t / 365.25)       # 年季节性
    price = 20 + 3 * np.sin(2 * np.pi * t / 90) + rng.normal(0, 0.5, days)
    promo = (rng.random(days) < 0.08).astype(float)    # 8% 天数促销
    holiday = np.isin(dates.month * 100 + dates.day, [101, 501, 1001]).astype(float)
    temp = 15 + 12 * np.sin(2 * np.pi * (t - 100) / 365.25) + rng.normal(0, 2, days)
    sales = (base + 0.05 * trend * t + weekly + yearly
             - 2.0 * (price - 20) + promo_lift * promo
             + 60 * holiday + 0.8 * (temp - 15)
             + rng.normal(0, 8, days))
    return pd.DataFrame({"date": dates, "sales": np.clip(sales, 5, None),
                         "price": price, "promo": promo,
                         "holiday": holiday, "temp": temp})

stores = {f"store_{i}": gen_store(42 + i, base=200 + 30 * i,
                                  trend=1.0, promo_lift=80)
          for i in range(3)}
print(stores["store_0"].head())
print(stores["store_0"].describe())

步骤 2:零样本点预测 + 分位带可视化

取最后 28 天做 holdout,前面全部做上下文,一行代码得到预测:

import matplotlib.pyplot as plt

H = 28
frames = {}
for name, df in stores.items():
    y = df["sales"].to_numpy(float)
    ctx, actual = y[:-H], y[-H:]
    fc = tfm.forecast([ctx], freq=[0])   # freq=0 表示日粒度
    point, (lo, hi) = fc[0][0], (fc[0][1], fc[0][2])
    frames[name] = (ctx, actual, point, lo, hi)

# 画扇形图
name = "store_0"
ctx, actual, point, lo, hi = frames[name]
plt.figure(figsize=(12, 4))
plt.plot(range(len(ctx))[-120:], ctx[-120:], label="历史")
plt.plot(range(len(ctx), len(ctx) + H), actual, label="真实")
plt.plot(range(len(ctx), len(ctx) + H), point, label="中位数预测")
plt.fill_between(range(len(ctx), len(ctx) + H), lo, hi, alpha=0.3, label="80%区间")
plt.legend(); plt.title(f"{name} 零样本预测"); plt.tight_layout(); plt.show()

输出结构说明:forecast 返回每个序列一条点预测向量和多条分位向量。扇形图中间线贴合真实值说明趋势抓得准,阴影带覆盖真实值说明不确定性估计合理。

步骤 3:多指标评估 + 和基准模型对比

from sklearn.metrics import mean_absolute_error, mean_squared_error

def smape(y, p):
    return 100 * np.mean(2 * np.abs(p - y) / (np.abs(y) + np.abs(p) + 1e-8))

def mase(y_true, y_pred, y_train, m=7):
    naive = np.mean(np.abs(y_train[m:] - y_train[:-m]))
    return np.mean(np.abs(y_true - y_pred)) / (naive + 1e-8)

def pinball(y, q, tau):
    diff = y - q
    return np.mean(np.maximum(tau * diff, (tau - 1) * diff))

def picp(y, lo, hi):
    return np.mean((y >= lo) & (y <= hi))

for name, (ctx, actual, point, lo, hi) in frames.items():
    # 季节naive基线:用7天前同一星期值
    base = ctx[-H-7:-7]
    print(f"== {name} ==")
    print(f" TimesFM  MAE={mean_absolute_error(actual, point):.2f} "
          f"RMSE={mean_squared_error(actual, point) ** 0.5:.2f} "
          f"sMAPE={smape(actual, point):.2f} MASE={mase(actual, point, ctx):.3f}")
    print(f" 基线    MAE={mean_absolute_error(actual, base):.2f} "
          f"MASE={mase(actual, base, ctx):.3f}")
    print(f" pinball(q90)={pinball(actual, hi, 0.9):.2f} 覆盖率PICP={picp(actual, lo, hi):.2%}")

一般结论:TimesFM 的 MASE 明显小于季节 naive(通常改善 15%~30%),80% 区间覆盖率落在 70%~90% 之间属于健康状态。

步骤 4:批量推理与门店级汇总

contexts = [df["sales"].to_numpy(float)[:-H] for df in stores.values()]
fcs = tfm.forecast(contexts, freq=[0] * len(contexts))
for name, fc in zip(stores, fcs):
    print(name, "预测均值:", round(float(np.mean(fc[0])), 1))

一次 forecast 传入全部序列即批量推理,吞吐随 batch size 线性提升,适合上百门店 nightly job。

步骤 5:滚动回测

origins = [500, 560, 620, 670]   # 4 个历史切点
rows = []
y_all = stores["store_0"]["sales"].to_numpy(float)
for o in origins:
    ctx = y_all[:o]
    actual = y_all[o:o + H]
    fc = tfm.forecast([ctx], freq=[0])[0][0][:H]
    rows.append(mase(actual, fc, ctx))
    print(f"切点 {o}: MASE={rows[-1]:.3f}")
print(f"平均 MASE={np.mean(rows):.3f} ± {np.std(rows):.3f}")

若某个切点误差突增,回看该窗口往往对应大促或节假日,说明单一 holdout 不可靠,滚动回测给出的是误差分布而非运气值。

步骤 6:上下文长度消融

for L in [64, 128, 256, 512]:
    ctx = y_all[-(H + L):-H]
    import time; t0 = time.time()
    fc = tfm.forecast([ctx], freq=[0])[0][0][:H]
    dt = time.time() - t0
    print(f"上下文 {L}: MASE={mase(y_all[-H:], fc, ctx):.3f} 耗时={dt:.2f}s")

经验规律:上下文从 64 增至 256 时精度提升明显,超过 512 后收益递减但耗时继续涨,生产环境取 256~512 是性价比甜点。

步骤 7:协变量融合(XReg)

把促销、节假日、价格作为已知未来信息输入:

df = stores["store_0"]
ctx = df["sales"].to_numpy(float)[:-H]
# 未来 H 天的协变量是"已知的"(排期表),历史段同步截断
xreg_hist = df[["promo", "holiday", "price"]].to_numpy(float)[:-H]
xreg_fut = df[["promo", "holiday", "price"]].to_numpy(float)[-H:]
fc_x = tfm.forecast_with_covariates([ctx], [xreg_hist], [xreg_fut], freq=[0])
print("融合协变量后 MAE:", mean_absolute_error(df['sales'].to_numpy(float)[-H:], fc_x[0][0][:H]))

大促周的误差通常下降最明显,因为纯历史外推无法预知”下周要降价”。

步骤 8:异常检测

resid = np.abs(y_all[:-H][-90:] - tfm.forecast([y_all[:-H-90:-H]], freq=[0])[0][0][:90])
thr = resid.mean() + 3 * resid.std()
flags = np.where(resid > thr)[0]
print("异常点相对位置:", flags, "阈值:", round(float(thr), 2))

思路是”预测即正常,偏离即异常”:用模型重构历史,残差超过均值 3 倍标准差即告警,适合设备监控、销量作弊发现。

步骤 9:长周期预测与吞吐调优

fc_long = make_forecaster(horizon=90).forecast([y_all], freq=[0])
print("90天预测长度:", len(fc_long[0][0]))

长视界误差天然发散,建议同时输出分位带给业务方”乐观/中性/悲观”三档。吞吐方面:CPU 上 per_core_batch_size 设为 16~64,序列先按长度分桶再批量送入,可减少 padding 开销;QPS 不够再考虑 GPU 或量化。

五、常见坑

  1. 频率参数填错freq 决定季节性先验,日数据填 0、周数据填 1、月数据填 2,填错会导致季节模式整体错位,先拿季节 naive 对拍验证。
  2. 上下文带入未来信息:回测切分必须严格 ctx=y[:o],归一化统计量也只能在 ctx 内计算,否则回测成绩虚高、上线即崩。
  3. 只看 MAE 不看覆盖率:点预测准但区间过窄等于对风险视而不见,PICP 和 pinball loss 必须同步达标。
  4. 协变量未来值用预测值冒充真实排期:促销排期是已知的,但未来气温、未来价格常需另行预测,把带噪预测当真值输入会污染结果,建议做敏感性测试。
  5. 异常阈值全局一刀切:不同门店销量量级差数倍,阈值必须按序列单独估计,或改用分位数残差。
  6. 长视界当短期用:90 天预测的后半段带宽很宽是正常的,不要用它做精确补货,只做趋势备货参考。

六、总结

本文用一份仿真零售数据走通了 TimesFM 2.5 的完整工作流:零样本点预测开箱即用,分位带给出不确定性,多指标评估避免自嗨,滚动回测验证稳定性,上下文消融找到性价比点,XReg 融合已知未来事件,残差法顺手做异常检测。生产落地的推荐姿势是: nightly 批量任务 + 256 点上下文 + 28 天视界 + 协变量排期表 + MASE/PICP 双监控。掌握这套流程后,新增门店或新 SKU 无需训练即可上线预测,真正把预测从”训模型”变成”调配置”。

参考资料:TimesFM 官方 GitHub 与 MarkTechPost 端到端教程(1行)。

七、深度扩展:把评估与部署做扎实

7.1 六个指标到底在回答什么问题

MAE 回答”平均偏多少”,对异常值稳健,适合向业务方汇报;RMSE 对大误差平方惩罚,适合对缺货零容忍的场景;sMAPE 是对称百分比误差,跨门店横向对比时不受量级影响,但注意分母接近零时会失真,低销量 SKU 慎用;MASE 用季节 naive 做分母,大于 1 意味着”还不如上周同期照抄”,是模型是否及格的生死线;pinball loss 评价单条分位线,q90 的 pinball 高说明上界估计激进或保守;PICP 评价区间整体,80% 区间覆盖 80% 真实值最健康。生产监控建议固定看 MASE + PICP 双指标:MASE 管精度,PICP 管风险。

7.2 协变量工程的三个实战细节

第一,未来协变量分”已知”与”需预测”两类:促销排期、节假日历是已知的,直接用真值;未来气温、未来价格是需预测的,建议用保守估计并做敏感性测试——把气温上下浮动 3 度各跑一遍,看预测波动是否在可接受范围。第二,协变量要做滞后对齐:今天降价对销量的拉动可能延续 3 天,构造 promo_lag1/2/3 三列比只给当日促销效果更好。第三,警惕协变量泄漏:训练段协变量只能用当时已知的信息重构,不能用事后修正版,否则回测虚高。

# 滞后协变量构造示例
df["promo_lag1"] = df["promo"].shift(1).fillna(0)
df["promo_lag3sum"] = df["promo"].rolling(3).sum().fillna(0)

7.3 长视界预测的三档用法

90 天预测不要只给一条线,给业务”乐观/中性/悲观”三档:中性取 q50,乐观取 q90,悲观取 q10。补货决策用中性档,安全库存按”乐观减中性”的差值计提,大促备货看乐观档。这样预测的不确定性直接变成库存语言,业务方第一次觉得预测报告”能用”。

7.4 生产部署 checklist

nightly 任务顺序:拉数→校验(断点/负值/缺失)→分桶批量推理→写库→算 MASE/PICP 监控→超阈值告警。模型版本固定(checkpoint hash 入库),输入数据快照保留 30 天,任何一次口径变更走回测对比。QPS 瓶颈先从 batch size 与序列分桶下手,实在不够再上 GPU,不要一上来就加机器。

八、总结(扩展版)

TimesFM 2.5 把时序预测从”训模型”变成”调配置”,但配置调得好不好,取决于评估是否诚实、协变量是否干净、监控是否到位。本文的完整链路就是一套诚实评估的方法论:滚动回测代替单点 holdout,分位指标代替唯 MAE 论,敏感性测试代替对未来的盲目信任。把这套方法跑顺,新增门店与新 SKU 零样本上线只是起点,真正的收益是整个预测体系的可解释与可运维。

九、附录:评估脚本一体化与排障手册

把评估沉淀为单个可复现脚本是生产落地的关键一步。下面给出回测主循环的完整形态:外层遍历切点,内层做预测与指标累计,最后输出均值与标准差,结果写入 CSV 供复盘。排障时按”数据→配置→模型→指标”顺序排查:先看输入序列有无断点与负值,再看频率参数与上下文长度是否合理,接着检查协变量对齐是否错位,最后才怀疑模型本身。九成以上的”模型不准”最后定位都是数据与配置问题。另一条经验是保留每次回测的预测快照:当业务方质疑某周预测偏差时,能拿出当时的输入快照与分位带自证,而不是重新跑一遍得到不一样的结果。快照加不可变存储,是预测系统可解释性的底座。版本管理同样重要:模型 checkpoint 的哈希、代码 commit、数据快照日期三者绑定入库,任何一次口径变更都走一遍完整回测并生成对比报告,报告中必须同时包含点预测指标与区间覆盖率,避免只看单一指标得出片面结论。做到这些,预测系统才算从实验品变成生产品。

© 版权声明

相关文章

暂无评论

暂无评论...