时序预测这行以前的规矩是一个数据集训一个模型,调参调到吐。
TimesFM 是 Google Research 那个拿来就能零样本预测的基座模型,仓库我扒了一遍。
想写它不是因为分数,是因为「变长、缺洞、要分位数」这几件脏活它全在模型外面办掉了,代码里看得清清楚楚。

定位

decoder-only 的 patch 化时序模型,论文进的 ICML 2024,开源仓库 Apache-2.0。它自己的话是零样本精度接近逐数据集单独监督训练的水平——注意是接近,不是超过,这个定位一开始就没吹。

现在这东西不只是个 repo:BigQuery ML 里能用 SQL 调,Connected Sheets 里能在表格里拉预测,Vertex Model Garden 有 dockerize 好的端点。倒是仓库自己写着,开源这版不是 Google 官方支持的产品。

两套版本号

第一个坑在这儿:库的版本号和模型的版本号是两套,而且库的还更小。

pyproject.tomlversion = "2.0.2"(2026 年 7 月 2 日发的),而最新模型是 TimesFM 2.5(2025 年 9 月)。pip install 装的是库,from_pretrained 拉的是模型权重,两者各走各的号。1.0 与 2.0 的代码在 v1/ 子目录里存档,真要加载老模型得 pip install timesfm==1.3.0 退回旧库。

2.5 相对 2.0 的变化值得记:参数从 500M 降到 200M,上下文从 2048 升到 16384,多了一个可选的 30M 分位头,然后把 frequency 指示器整个去掉了——以前调用方得告诉模型这序列是日频还是月频,现在不用了。

最短路径

bash
pip install timesfm[torch]   # 或 timesfm[flax];要协变量再加 timesfm[xreg]
python
import numpy as np
import timesfm

model = timesfm.TimesFM_2p5_200M_torch.from_pretrained("google/timesfm-2.5-200m-pytorch")
model.compile(
    timesfm.ForecastConfig(
        max_context=1024,
        max_horizon=256,
        normalize_inputs=True,
        use_continuous_quantile_head=True,
    )
)
point, quantiles = model.forecast(horizon=12, inputs=[np.linspace(0, 1, 100)])

compile() 不是可选步骤。forecast() 开头就查 compiled_decode 是否为空,空就抛 RuntimeError。而 max_contextmax_horizon 是在这一步定死的,之后想换长度得重新 compile。

输出是两个数组:点预测 (B, H),分位 (B, H, 10)——第 0 列是均值,后面九列是 10 到 90 分位。

一次预测的来回

forecast() 里最值得看的是它替调用方擦了多少屁股。

进来的序列可以是任意长度、可以带 NaN。它先 strip_leading_nans 把开头那段连续的 NaN 整体丢掉,剩下中间的洞用 linear_interpolation 线性插值补上。然后对齐到定长:比 max_context 长的直接留最后那一段,短的在左侧补零并附一条布尔 mask 说明哪些位置是填充。batch 凑不满 global_batch_size 就拿 [0, 0, 0] 这种哑序列垫上,返回前再按原始数量裁掉。这一整层调用方完全看不见。

模型自己只吃定长。往里走是 32 点切一块(input_patch_len = 32),经一个残差块从 64 维升到 1280 维,进 20 层解码器主干——1280 维、16 头、RoPE 位置编码、注意力与前馈都用 RMSNorm、qk 也做 norm、swish 激活、qkv 融合、全程无 bias。

出来的一块是 128 点(output_patch_len = 128)。输出块比输入块长四倍是有意的:长程预测要滚的自回归轮数就少四倍,误差少攒几轮。horizon 在 128 以内一趟出完,超了才把已生成的接回上下文再解一轮。

分位数走的是另一条路。那个可选的连续分位头一次吐满 1024 点 × 10 列,不参与自回归——output_projection_quantilesoutput_dims 就是 10240,1024 × 10 摆在那儿。这就是为什么 2.5 敢说「连续分位预测到 1k horizon」。

那几个开关

ForecastConfig 上那串 flag 每一个都对着一个真实的预测学问题,docstring 写得比我转述的清楚:

normalize_inputs:逐序列做 RevIN,量级极大或极小时防数值问题。
use_continuous_quantile_head:用独立的连续分位头,避免分位塌缩。
force_flip_invariance:模型默认保证 a ≥ 0TimesFM(aX + b) = a·TimesFM(x) + b,这个开关把它扩到 a < 0
infer_is_positive:输入非负则保证输出非负。
fix_quantile_crossing:修分位交叉。
return_backcast:把模型对上下文的重建一并返回,协变量那条路要它。

fix_quantile_crossing 的实现是从中位数往两边压:索引 4 到 1 一路取 min,6 到 9 一路取 max。交叉是被抹平了,但抹平不等于这个分布本身自洽——它只是让你不至于看到 30 分位比 70 分位还高这种没法交差的东西。

协变量的两种拼法

forecast_with_covariates() 支持动态与静态、数值与类别共四类协变量,底下是个带 ridge 的线性模型(XReg),跟 TimesFM 有两种拼接顺序:

"xreg + timesfm" 先用线性模型拟合目标本身,再让 TimesFM 去预测残差。"timesfm + xreg" 反过来,先让 TimesFM 出预测,再用线性模型拟合它的残差。前者适合协变量能解释大头的场合,后者适合协变量只做微调。

这条路要求 compile 时把 return_backcast 设成 True,否则进去就抛——因为拟合残差需要模型对上下文那一段的重建值。

两处代价

force_flip_invariance 默认是 True,而它的实现是:拿 -inputs 再跑一遍完整 decode,把分位轴翻过来,然后取 (x - x_flipped) / 2。也就是说默认配置下每次预测跑两趟前向。这个对称性保证值不值两倍算力,得看你的场景;不值就显式关掉。

另一处是 window_size。它在 ForecastConfig 里老老实实声明着,docstring 写的是 The window size for decomposed forecasting. TODO(siriuz42): implement it.——我把整个仓库搜了一遍,除了声明它自己那两行,没有任何一处读它。这是个纯占位的配置项,别照着字面理解去指望它做分解预测。

边界

上下文 16384 是硬上限,超了的处理方式是留最后 16384 点,不是滑窗聚合——真有几十万点的序列,得自己想怎么降采样或者分段。

max_contextmax_horizon 编译期固定,服务里如果不同请求需要不同长度,要么按最大值编译(短序列白填零),要么维护多个编译实例。

分位头是可选的 30M,不开就退回主干那十列输出,容易塌缩——这是它专门加一个头的原因,所以想要像样的区间预测就别省这 30M。

读后

零样本时序预测这个方向我原本是有点怀疑的,因为时序不像语言,不同领域之间的「语法」差得远。读完仍然不敢说它在所有场景都够用,但至少 forecast() 那三十行预处理让我服气:变长、缺洞、批不齐这些事,谁做过谁知道有多烦,它一句话都没让调用方操心。

至于那个搜遍全仓库没人读的 window_size,倒是很亲切——这种「声明了就当实现了」的东西,我自己代码里也有几处,只是没胆子把 TODO 留在公开的 docstring 里。