最近有个做运营的朋友拿着一份销量预测需求来找我,说业务方已经厌倦了“拍脑袋备货”,希望用历史数据把未来两周的量算出来。聊到最后他问我,深度学习做时序预测,是不是直接上 Transformer 就行。我告诉他,如果你直接把数据塞进一个 LSTM 或者 Transformer,大概率会在上线后的第一周就被业务方叫去喝茶。
这个问题表面是模型选型,背后其实是一整套方案:数据怎么准备、窗口怎么滑、模型怎么训、指标怎么算、上线之后怎么维护。这篇文章我打算把做时序预测项目的完整技术链路拆开讲一遍,重点讲那些按文档复现时容易翻车、但在业务里真正决定成败的细节。如果你刚开始接触深度学习时序预测,或者已经用开源库跑通了 demo 但一上真实数据就表现稀烂,这篇应该能帮你省下不少排查时间。
1. 为什么时序预测值得用深度学习而不是传统统计模型
1.1 传统时序方法的能力边界:ARIMA、指数平滑与 Prophet
先别急着否定传统方法。ARIMA、Holt-Winters、Prophet 这些东西到今天依然有它们的用武之地,尤其是在样本量小、季节性稳定、单变量、可解释性要求高的场景里,它们简单可靠,上线成本极低。但它们的边界也很明显,ARIMA 对平稳性要求高,实际业务数据普遍带有趋势、季节和节假日效应,经常要做差分、识别阶数、做残差诊断,一套流程走下来耗时不少,而且面对多变量输入时基本只能做线性外生变量扩展,很难处理变量间的非线性交互。
Prophet 对节假日和趋势的刻画比较友好,适合快速出一版可解释的结果,但它本质上还是把时序拆成趋势、季节、节假日项再加回归项,对高维特征和高频随机波动的拟合能力有限。指数平滑类方法则更适合模式固定、波动小的序列。换句话说,传统方法擅长回答“数据按既定模式继续走下去会发生什么”,但不太擅长回答“当多个变量一起变化时会发生什么”。
1.2 深度学习的真正优势:高维特征、非线性与多变量交互
深度学习之所以值得用,核心优势不在于“模型更复杂”这种表面理由,而在于三点。第一,端到端特征表达:模型可以自动从原始序列里抽取周期、趋势和突变模式,减轻了手工特征工程的压力。第二,天然支持多变量输入:你可以在同一个模型里输入销量、价格、促销标记、天气、节假日等多个序列,模型自己去学它们之间的交互关系,这在传统统计模型里很难做到。第三,长依赖建模:TCN、Transformer 这类结构可以捕获几百甚至上千时间步之前的依赖信息,非常适合流量预测、设备寿命预测这类需要看较长上下文的场景。
另外,深度学习还有一个被低估的优势:概率输出。你可以用分位数损失或 MC Dropout 让模型输出一个预测区间,而不只是一个点值。这个能力在库存备货、容量规划等场景里非常实用,因为业务方不只想知道“下周销量是多少”,他们更想知道“下限和上限在哪”,以便做安全库存决策。
1.3 什么时候不该用深度学习
我必须先泼一盆冷水。深度学习不是银弹,下面几种情况我建议你老老实实用传统方法或者树模型。第一,样本量太少,几百条到一两千条序列数据,深度学习很容易过拟合,随便一个带正则的线性回归或 XGBoost 都可能表现更好。第二,强可解释性要求,如果业务方需要你解释“为什么预测值是这个数”,深度学习会让你很难交代。第三,边缘低功耗设备,算力和内存限制下,简单模型几乎总是更可靠的选择。第四,开发资源有限,深度学习方案的调试成本远高于传统方法,如果你只是需要一个“明天大概多少量”的结果,没必要上来就搭一套训练流水线。
我个人的习惯是先花半天时间用“手工滞后特征 + LightGBM”跑一个 baseline,再决定要不要上深度学习。如果树模型已经能到业务可接受的精度,那就没必要为了炫技上深度学习。如果 baseline 不够,你再带着已知的 baseline 去比较不同深度模型,也能更客观判断深度学习的增益到底在哪里。
2. 数据与特征准备:比模型选择更决定上限
2.1 滑窗构造的正确姿势:输入长度、预测长度与步长
几乎所有深度学习时序方案的第一步都是把原始序列切成“输入窗口-预测目标”的样本对。假设有一段长度为 N 的连续序列,输入窗口长度 input_len,预测长度 pred_len,步长 stride,那么你能得到的样本数量大约是(N - input_len - pred_len) / stride + 1。我自己第一次做这类项目时以为样本数应该等于序列长度,后来才发现样本数其实是窗口数量,这个认知直接影响你对数据量是否充足的判断。
窗口构造时最需要注意的是“不能混入未来信息”。你用来生成特征的数据必须截止到当前时刻 t,而预测目标必须从 t+1 开始。看起来像废话,但我在实际代码评审里真的见过有人把滚动均值算成了包含 t+1 在内的“未来均值”,模型在验证集上指标异常漂亮,一上线就崩,原因就是特征泄漏。
# 滑窗构造示意 import numpy as np def make_windows(data, input_len, pred_len, stride=1): X, y = [], [] for i in range(0, len(data) - input_len - pred_len + 1, stride): X.append(data[i:i + input_len]) y.append(data[i + input_len:i + input_len + pred_len]) return np.array(X), np.array(y) # data 是一个二维数组,shape = (时间步, 变量数) X, y = make_windows(data, input_len=168, pred_len=24, stride=1)关于输入长度怎么选,没有标准答案,但有一个经验法则:窗口长度至少要覆盖业务里最主要的周期。如果是小时级电商销量数据,7 天(168 个点)是一个不错的起点;如果是天级电力负荷数据,至少要含 7 天,最好能含 28 天以上。输入长度和预测长度的比例一般不建议小于 3:1,也就是说预测未来 24 小时,输入可以给到 72 到 168 小时。太短的输入会让模型看不到足够的上下文,太长的输入则会引入大量噪声,训练也会变慢。
2.2 归一化与特征工程的三个泄漏陷阱
时序任务里最常见的三个坑都藏在特征和归一化环节。第一个坑是归一化泄漏:用全量数据的均值和方差去做标准化,等于让模型在训练时“偷看”了未来分布的统计量。正确做法是先切分训练集和验证集,然后用训练集的统计量去 transform 所有集合。这个细节几乎所有教程都会提,但代码里落实不到位的人非常多。
# 正确做法 from sklearn.preprocessing import StandardScaler scaler = StandardScaler() train_scaled = scaler.fit_transform(train) val_scaled = scaler.transform(val) # 沿用训练集的参数 test_scaled = scaler.transform(test)第二个坑是滚动统计特征的错位。很多人喜欢给模型加“最近 7 天均值”这类滚动特征来帮助它捕捉趋势,但计算的时候容易忘记 shift。比如你想用 t 时刻及之前的信息预测 t+1,那么滚动均值应该截止到 t,而不是包含 t+1 或 t+2。这个错位一两行代码就能造成不可察觉的泄漏,最稳妥的做法是构造特征时明确要求所有特征都调用.shift(1)对齐到标签之前。
第三个坑是缺少时间戳的周期编码。深度学习模型不像树模型那样天然擅长处理星期几、第几月这类类别特征,直接把 timestamp 数值喂进去是错误示范。对于周期性特征我一般用 sin/cos 编码,比如小时可以用sin(2π*hour/24)和cos(2π*hour/24)分别编码,这样 23 点和 0 点之间的距离在数值空间上不会突然跳变,模型更容易学到周期连续性。星期、月份、节假日是否标记,则可以用 one-hot 或者 embedding 处理。
2.3 按时间顺序切分:walk-forward 验证为什么比随机 K 折合理
我见过太多人用train_test_split(X, y, random_state=42)处理时序数据,结果模型在验证集上表现近乎完美,但上线后一塌糊涂。原因很简单:随机切分会把未来的片段混进训练集,模型在训练时就看到了“答案”的一部分,这不是建模能力,而是记忆能力。时序数据的验证必须严格按时间顺序进行,一个经典做法是 walk-forward validation,也就是滚动地训练多个模型,每次用最近一段时间的窗口来验证。
具体操作是:假设你有 13 个月的日数据,第一次用前 6 个月训练,预测第 7 个月;第二次用前 7 个月训练,预测第 8 个月;依此类推,最后汇总所有验证月度的预测误差。代价是要训练多个模型,计算开销大,但这样得到的误差估计更贴近真实线上表现,也能直观看到模型的稳定性是否会随数据时间推移而衰减。如果不做这么重,至少也要保证训练、验证、测试三段时间严格不重叠,并且测试段一定是最新的数据。
训练区间 验证区间 含义 [1, 6月] 7月 第一批验证 [1, 7月] 8月 第二批验证 [1, 8月] 9月 第三批验证 ...2.4 静态特征怎么拼进序列模型
业务数据里除了时间序列本身,还有不少静态特征:比如 SKU 所属类目、商品的促销渠道、设备的型号编号等。这类特征没法直接塞进 LSTM 或者 Transformer 的序列维度里,常见做法是先把序列特征过一遍编码器拿到一个向量,再把静态特征拼接在这个向量后面,一起送到全连接输出层。如果你用 Transformer 那一类模型,也可以给静态特征加一个线性投影层,然后作为额外的 token 拼在序列 token 后面,让注意力机制自己决定怎么利用它们。
拼接位置对效果有影响,我的经验是“越早拼接越容易被序列部分忽略,越晚拼接越像决策层的外部提示”。实际项目里我习惯在编码器输出之后拼接,这样模型先用序列内部结构提取时序规律,再用静态特征修正最终输出,工程实现也比较干净,不存在“静态特征如何参与注意力计算”这类麻烦问题。
3. 模型选型的演进路径:LSTM、TCN 与 Transformer 类方法怎么选
3.1 LSTM/GRU:仍然稳健的基线
LSTM 和 GRU 是入门深度学习时序预测最稳妥的起点。它们天然适合序列建模,对中短序列的依赖关系捕获能力足够,训练流程成熟,框架支持到位。我在实际项目里通常直接用 GRU 而不是 LSTM,因为 GRU 参数更少,训练更快,效果往往不差,尤其在数据量不是特别大的时候,GRU 过拟合风险更低。
用这类循环模型做预测有两种常见的设计。第一种是编码器-解码器结构,适合输入和输出长度都能灵活变化的场景,但训练复杂度会高一些。第二种更简单:把最后一个时间步的隐状态取出来,接一个全连接层直接输出整个预测窗口。后一种最适合“定长输入、定长输出”的常规业务,模型简单,部署也简单。需要注意,这第二种设计虽然结构简单,但对损失函数和输入长度的选择更敏感,输入尺度差异大时收敛会很痛苦。
3.2 TCN:用因果卷积换并行与稳定
TCN(Temporal Convolutional Network)是我个人很喜欢的一个选择。它用一维因果卷积来建模时序,核心操作是膨胀卷积,每层的膨胀因子按 1、2、4、8 这样的规律递增,让感受野指数级扩大。相比 LSTM,TCN 有两个明显优势:一是训练可以并行,速度更快,二是梯度路径更短更稳定,不会像 RNN 那样容易出现长序列梯度问题。
在多个公开数据集上的对比实验里,TCN 的效果通常不输 LSTM,有些场景还更好。但 TCN 也有自己的坑,感受野的大小需要提前计算。如果你的输入窗口是 168,而 TCN 的感受野只有 64,那模型根本看不到完整序列,效果自然会差。选层数和膨胀率时要保证最后一层感受野能覆盖输入长度,这方面要自己算清楚,不要指望超参搜索帮你兜底。
3.3 Transformer 与 PatchTST:长序列下的新选择
Transformer 在时序预测上的热度很高,但直接套用 NLP 里的标准 Transformer 并不是最佳选择。原始 Transformer 的自注意力计算量与序列长度呈平方关系,序列长度上千之后训练和推理都会非常吃力。另外,Transformer 对时间步的位置编码也相当敏感,用简单的位置索引往往不如引入可学习的时间特征编码稳健。之前有一批专门针对时序的改进模型,比如 Informer、Autoformer 一类,核心思路都是降低注意力的计算复杂度和强化趋势/周期项的分解,在长序列基准上确实有提升。
近几年更值得关注的是 PatchTST 这类“先把序列切成 patch 再做注意力”的思路。它的做法是把每个变量的一维时序切成若干个长度为 P 的小块,相邻 patch 可以部分重叠,每个 patch 作为一个 token 进入 Transformer 编码器。这样做一方面让 token 数量大幅减少,注意力计算从“每个时间点互相看”变成“每段特征互相看”,复杂度显著下降;另一方面,patch 本身相当于做了局部降维和特征聚合,模型的归纳偏置更有利于捕捉局部模式,对不同变量的通道独立建模也让计算更可控。
不过我得说句实在话:如果你的输入窗口只有几十个点,或者数据总量就几万条,Transformer 类模型的优势很难体现出来,甚至可能不如一个调好的 TCN。长序列、大数据、多变量强交互,才是这类模型的发力场景。我见过不少项目一上来就堆 Transformer,结果训练慢、调参难、解释性差,最后效果还打不过 GRU。
3.4 一张表完成选型决策
| 方法 | 适用场景 | 训练难度 | 推理速度 | 长序列能力 | 备注 |
|---|---|---|---|---|---|
| 手工特征 + LightGBM | 小样本、强可解释、特征工程充分 | 低 | 极快 | 弱(需递归滚动) | 永远值得先跑 baseline |
| LSTM / GRU | 中短序列、多变量、数据量中等 | 中 | 快 | 中 | 默认起点,优先试 GRU |
| TCN | 中长序列、并行训练、稳定梯度 | 中 | 快 | 较强 | 注意感受野覆盖 |
| Transformer / PatchTST | 长序列、大数据、强交互 | 高 | 中 | 强 | 数据量不足时收益有限 |
3.5 一个真实但常见的误区:模型越新效果一定越好
在项目里我经常被问到“你这个用的哪个最新模型,怎么不用最新的那个”。模型选型确实要关注新方法,但不代表新方法在任何场景都强。某个在某论文数据集上 SOTA 的模型,换到你的业务数据上很可能表现平平,因为业务数据往往有大量噪声、缺失值、不规则采样和外部事件干扰,这些在开源基准数据集里都被“清洗得过于干净”。我的建议是给每个模型跑 3 到 5 个随机种子的平均结果,而不是只跑一次就下结论。时序模型受初始化影响很大,同一个模型换一个随机种子,误差波动可能达到 5% 以上,这时候一两次实验的对比没有参考价值。
4. 训练与评估:多步预测策略和指标里的坑
4.1 直接多步、递归多步与多输出头
预测未来多个时间步是一个需要专门设计输出形式的问题,很多人第一个想到的是递归预测:先用模型预测 t+1,再把预测值塞回输入预测 t+2,如此滚动。但这样做的最大问题是误差累积,预测得越远,前一步的误差会被不断放大,最终结果常常偏差大得离谱。我见过误差在第二步就开始失控,到第五步直接变成一个不可用的值的典型场景,尤其是数据噪声大的业务序列。
相比之下,直接多步预测是更稳的选择:你可以训练多个模型,每个模型负责预测某一个时间步,或者用一个模型带多个输出头,最后一步之前并行输出整个预测窗口。还有介于两者之间的 DIRRec 思路,把递归和直接结合起来。实际工程里我最常用的是多输出头,也就是输入一个窗口,输出一个长度为 pred_len 的向量。这样训练和部署都简单,误差不会随时间步传递,只是输出头共享了大部分编码器,对不同的时间步的独立性会有限制,但实测下来仍然比纯递归稳健得多。
4.2 损失函数与分位数损失
回归任务最常见的选择是 MSE,它对较大误差惩罚更重,预测结果会更“平滑”,缺点是对异常值敏感。MAE 则对异常值更稳健,但收敛速度略慢。实际业务数据总会有促销带来的尖峰、偶发的缺货、天气造成的异常,完全用 MSE 会让模型为了压低一次离群大误差而牺牲整体精度。我一般先快速用 MSE 跑通流程,再对比 MAE 或 Huber 损失看验证集指标,Huber loss 相当于两者的混合,对异常值有容忍度,梯度也平滑,是个不错的折中方案。
如果你需要的不只是点预测,还要给业务方一个区间,可以直接用分位数损失。做法是让模型同时输出多个头,分别优化 0.1、0.5、0.9 分位数的损失。分位数损失的定义是对“低于目标分位数的误差”和“高于目标分位数的误差”赋予不同权重,这样模型学到的不再是条件均值,而是条件分位数。这在库存场景特别好用,业务方可以按 0.9 分位数备货,把缺货风险压到一个可接受的水平。
def quantile_loss(y_true, y_pred, q): error = y_true - y_pred return np.maximum(q * error, (q - 1) * error)4.3 评估指标一定要在原始尺度重算
很多人在训练时用归一化后的数据算 loss,评估的时候也顺手算了 MAE 或 RMSE,得到一个小数就以为模型很好。这是大忌。归一化会隐藏真实的误差水平,你必须把预测值和真实值都反归一化回业务原始尺度,再计算指标,这样才有业务含义。比如销量预测误差 5 件,在归一化空间里可能是“0.01”,根本看不出模型能不能用。
还有 MAPE 这个指标,名字叫平均绝对百分比误差,公式是mean(|(y_true - y_pred) / y_true|),看着直观,但真实业务数据里如果某个时刻真实值接近 0,MAPE 就会变成一个极大的数,把整体误差完全带歪。零值多的序列建议直接用 SMAPE 或者加一个极小值平滑项。我个人更推荐结合业务看加权指标,比如对销量高的时段重点考察误差,或者直接用分位数覆盖率来判断区间预测质量。评估的另一个关键是模拟线上滚动预测,而不是只预测验证集最后一段,因为线上是“一边来新数据一边预测”,不是一次性的静态预测。
def smape(y_true, y_pred): return 100 * np.mean(2 * np.abs(y_pred - y_true) / (np.abs(y_true) + np.abs(y_pred) + 1e-8))5. 从实验到线上:部署、监控与增量更新的现实问题
5.1 模型导出与推理延迟的取舍
模型在离线实验里跑得好,只是第一步。到了上线阶段,你首先关心的是推理延迟和模型体积。PyTorch 训练好的模型,可以直接通过 ONNX 导出成静态图,部署到 CPU 上也能获得不错的推理速度。对于一天跑一次批预测的场景,推理延迟根本不是瓶颈,几十毫秒完全可以接受。如果你要做的是高并发的实时预测,比如每一秒都要对几千个设备预测一次,那就要考虑 TensorRT 这类加速方案,甚至在模型设计阶段就控制参数量。
模型的体积也需要注意,LSTM 和 TCN 通常体积都很小,几百 KB 到几 MB 就能搞定。Transformer 类模型参数多,压缩和量化要更谨慎。时序预测对量化精度比图像分类更敏感,我建议如果要用 int8 量化,先在验证集上做一轮对比,误差增幅超过 5% 就不要量化了,不值得为省那点内存牺牲业务精度。
5.2 线上监控:预测残差与特征漂移
上线只是开始,真正考验方案的是运行一段时间之后的表现。线上环境的分布会变,最常见的是数据漂移:用户行为变了、促销节奏变了、设备工况变了,模型当初学到的规律会失效。我的经验是至少要监控三样东西:预测残差分布、输入特征分布、预测结果的稳定性。残差分布可以用滚动窗口统计均值和方差,如果连续一周的均值偏移超过阈值,就该触发重训流程。
输入特征漂移检测可以用一些现成的统计方法,比如对关键特征做 K-S 检验,或者直接看特征均值的滚动变化。这里有个容易被忽略的细节:监控预测值和真实值之间的时序对齐。线上数据落库经常有延迟,如果预测时刻和真实值时刻没对齐,计算出来的残差会整体偏移,会误导你的告警。所以数据管道里一定要给每条预测记录打上“预测生成时间”和“预测目标时间”两个时间戳,后面排查问题会方便很多。
5.3 增量重训练的节奏与灾难性遗忘
重训练本身也是有坑的。很多团队选择每天用昨天的数据增量训练一次,但直接把新数据喂给旧模型做梯度更新,容易出现灾难性遗忘:模型记住了新数据,却把学到过的历史规律忘光了,整体指标反而下降。更安全的做法是保留一个“候选模型”,它在包含历史数据和新数据的完整训练集上重新训练,经过验证集评估之后,再决定要不要替换线上模型。
重训练频率不是越高越好,要根据数据变化速度和成本来定。我的经验是:如果数据变化是缓慢的季节性波动,一周重训一次足够;如果促销活动、市场环境变化频繁,可能需要每天训练一次,但此时要特别注意数据质量,因为单日异常数据可能误导模型。最后一招是给训练数据做时间衰减加权,越近的样本权重越大,这样模型自然不会完全忘了旧模式,又能跟上新趋势。
实际操作下来,我最大的体会是:在深度学习时序预测项目里,数据切分、特征对齐、评估方式和重训节奏,对最终效果的影响远远大于换一个更复杂的模型。你花一周时间调模型结构得到的提升,可能都不如把评估指标改对之后得到的“提升”来得实在。所以我通常会先花时间搭一个干净的数据流水线,再用 LightGBM 跑通 baseline,最后才轮到深度学习模型登场。除非数据量大、变量交互复杂、长序列依赖明显,否则真的没必要一上来就上 Transformer 全家桶。如果你正在做一个时序项目,建议先回去检查一遍你的验证集是不是按时间切的、归一化有没有泄漏、特征有没有错位——这三个点排查完,模型效果通常就已经比之前好不少了。