如果你做过真实场景的销量预测或流量预测,大概率碰到过这种诡异的情况:模型在验证集上 RMSE 漂亮得能截图发朋友圈,一上线赶上平台大促或者突发天气预警,预测曲线立刻飞到大洋彼岸。很多团队的归因是“序列有异常点”,但我这几年做非平稳时序预测的体会是,所谓异常,多半是因为现实世界发生了具体事件,而模型根本没看到事件。EVENTTSF 想解决的就是这件事——让预测模型具备事件感知能力,把节假日、促销、临时检修、气象预警这类扰动,从“残差里的一个尖峰”变成“显式的条件信号”。
这篇文章不是论文复现,更像是我把事件感知这套思路从前到后完整梳理一遍:非平稳序列到底难在哪,事件怎样从零散的日历提醒变成模型可学习的向量,事件分支如何在网络结构中和主干协同,以及训练评估、上线部署时那些坑。适合正在做销量、流量、能耗、气象、电力负荷等非平稳预测,并且已经被“平稳性假设被打脸”折腾过的人。
1. 先把问题说透:非平稳序列的漂移从哪来,事件为什么是主凶
1.1 非平稳不是脏数据,是有结构的条件变化
很多人一听到“非平稳”,第一反应是“这序列不干净,得先差分或者平滑”。但严格来说,平稳性要求的是整个序列的联合分布不随时间平移而改变,弱平稳则要求均值、方差恒定,自协方差只和间隔有关。现实里的业务数据几乎没有一个满足,尤其是带有周期性、趋势性和事件扰动的序列。
我早期也偷过懒,把序列直接丢给 LSTM,指望深度学习自己把趋势学出来。确实能学,但有一个边界:当序列的均值在某个时刻被外部力量硬推着挪了一段位置,纯靠历史模式的模型会拿最近的窗口硬套,结果要么反应极慢,要么跟着噪声乱跳。这不是模型的错,是信息不完备——模型不知道“外面发生了什么”,自然会把结构性的偏移当成随机扰动。
1.2 事件引发的漂移,通常有三个阶段
把常见的业务扰动拆开看,会发现事件对序列的影响不是“当天一个尖峰”那么简单,而是一个完整的过程。我在内部项目里习惯把事件的影响分成三个阶段,分别叫前兆、爆发、消化。
- 前兆效应:发生在事件开始之前。典型如电商促销,用户知道要打折,会把需求提前憋住甚至提前下单,序列在活动开始前就开始偏离基线。
- 爆发效应:事件窗口内,需求或负荷集中释放,出现陡峭的峰值或谷值。
- 消化效应:事件结束后,序列往往不会立刻回到原水平,而是先回落甚至低于基线,把前面积累的余量消化掉。
如果模型只把“促销当天”设成哑变量,它永远看不到前三天已经上扬的尾巴,最后只能靠把噪声调大来拟合。这也是我后来坚持用“事件窗口”而不是“事件点”来建模的根本原因。
1.3 漂移的三种形态:均值、方差、结构
非平稳性不能一概而论,至少可以分三种形态。
- 均值漂移:整体水平被抬升或压低,最常见,比如寒潮让用电负荷整体跳升。
- 方差膨胀:事件发生时段,序列的波动幅度明显大于平时,比如大促期间的分钟级订单量。
- 谱结构变化:周期性和自相关性发生改变,比如突发事件把正常的按周节奏打断。
事件感知模型的核心价值在于,事件特征可以对上述三种变化同时建模:均值漂移靠事件增量分支捕捉,方差膨胀靠时间位置和强度特征表达,谱结构变化则可以由事件窗口掩码来约束模型在不同状态之间切换。理解了这个,再看后面的事件特征化设计,逻辑就顺了。
2. 事件特征化:从日历提醒到模型能读的向量
2.1 结构化事件的四个基本要素
现实中的事件信息大多来自运营日历、活动排期、气象台预警、设备检修计划等系统。要把它们变成模型特征,我一般先抽四个要素:事件类型、开始时间、结束时间、强度等级。
- 事件类型:用类别 ID 或名称表示,比如“平台大促”“法定假日”“极端天气”“临时检修”。
- 开始/结束时间:精确到小时或最小预测粒度,用于计算相对位置。
- 强度等级:可以是连续值,也可以是“轻微/中等/强烈”分档,反映影响程度。
- 影响范围:地域、渠道或客户群,尤其在多序列建模时很有用,但单序列场景可以先忽略。
有了这四个要素,下一步就是设计编码方式。最容易犯的错是直接做一个 One-hot 事件向量拼到输入上,这么做在事件稀疏时几乎学不到东西。我建议把事件先转成“逐时间步的相对位置特征”。
2.2 用相对时间位置替代哑变量
相对时间位置是我的核心编码方式。对每个时刻 t,计算它到所有事件边界的距离,然后做分段线性变换。常用的几组特征如下。
- 距事件开始时间:t − start,正数表示事件已经发生,负数表示还有多久开始。
- 距事件结束时间:t − end,用于表达消化阶段。
- 剩余持续时间:max(0, end − t) / duration,归一化到 0 到 1 之间。
- 事件窗口掩码:是否落在 [start − 前兆天数, end + 消化天数] 之间。
这些特征叠在一起,模型就能区分“事件还有两天开始”“事件正在进行到中段”“事件刚结束半天”三种完全不同的状态。相比哑变量,这种编码方式给模型提供的是连续梯度,门控模块可以学出一个平滑的增量曲线。
2.3 强度衰减曲线与另一类非结构化事件
单点强度不够,事件的实际影响很少是方波,更常见的是快起慢落或者带铃形。我通常会预设几种强度曲线模板,比如三角形波、高斯型、指数衰减型,然后让模型通过一个轻量网络去选择或融合。这样门控增量的形状就有了先验约束,不会在事件窗口内剧烈震荡。
另一类是非结构化事件,比如新闻、公告、舆情帖子。这类信息没有明确的开始和结束时间,但同样会影响序列。处理思路是:先用文本编码模型抽事件向量,再按时间窗口做聚合,得到每日或每小时的“事件语义向量”,与结构化事件特征拼接。实测下来,对突发的舆情类事件,这个文本分支能提前几小时捕捉到异常信号,比只靠序列本身有效得多。
3. 把事件织进预测网络:EVENTTSF 的模块化设计
3.1 主干编码器的选择
EVENTTSF 的事件感知能力理论上可以叠加到任意序列主干上,TCN、Transformer、LSTM 都行。我的建议是别在主干上花太多纠结时间,先把基线模型跑准,再往上面接事件模块。我自己的默认选择是 TCN,因为感受野可控、训练稳定,在长序列上不会出现 Transformer 那种注意力权重稀疏的问题。
3.2 门控增量分支如何工作
事件模块的核心思路并不复杂:主干只负责预测“如果没有事件,序列应该长什么样”,事件分支负责输出一个增量,两个部分相加得到最终预测。关键是这个增量不能乱加,它必须只在事件窗口附近生效。
具体做法分两步。第一步,事件分支对相对时间位置、强度、语义向量做浅层卷积,得到逐时间步的事件状态。第二步,用一个门控把事件状态转换成增量,门控的取值由事件窗口掩码和一个置信度系数共同控制。窗口掩码保证事件不会污染非事件时段,置信度系数由模型根据事件特征自己学出来,相当于让模型判断“这个事件值不值得信”。
下面是一段简化后的核心逻辑,用 PyTorch 风格写出来,便于理解整体流程。
# event_state: [batch, seq_len, event_dim] # h_main: [batch, seq_len, hidden_dim] 是主干输出的隐藏状态 # 事件分支先做特征融合 event_feat = swish(conv1d(event_state)) # 计算门控置信度,加 sigmoid 约束到 0~1 gate_logit = linear(cat(h_main, event_feat)) gate_conf = sigmoid(gate_logit) # 事件窗口掩码:在前后兆窗口内为1,其余为0 window_mask = get_event_window_mask(event_feat) # 最终事件增量,值是受限的 delta = window_mask * gate_conf * tanh(linear(event_feat)) # 最终预测 = 主干的基线预测 + 事件增量 y_hat = y_base + delta这个设计的优点是把“基线”和“事件修正”解耦,训练时可以分别监督。如果你希望让事件增量更平滑,还可以在 loss 里加一个 delta 的时间差分正则,让相邻时刻的增量不能跳变太猛。
3.3 为什么这比“直接把特征拼进输入”更稳
直接拼特征的问题在于,事件特征属于稀疏外生变量,主干网络在反向传播时很容易忽略它,尤其是当序列自相关性比较强时,梯度信号会被历史窗口的强 mode 淹没。门控增量分支则不同,它天然把注意力集中在“哪些时段该被改写”,而不是和主干抢特征表达空间。
另外,解耦结构带来了可解释性。上线后如果预测偏了,你可以打开事件分支看门控值,直接知道模型是低估了事件强度,还是根本没有激活事件窗口。这个能力在排查故障时非常救命。
4. 数据准备与指标口径:训练事件感知模型最容易翻车的地方
4.1 采样对齐与事件样本平衡
事件感知模型训练时最常见的错误,是从整段序列里随机切窗口。这样会导致事件样本在训练集中占比极低,模型自然偏向“假装没有事件”。我的做法是事件中心采样:遍历每一个事件窗口,在它附近切固定数量的训练样本,再配同等数量的非事件样本。这样正负样本比例可控,门控模块才能充分见过事件。
采样时还要注意,不要让训练窗口跨过太多个事件。如果窗口长度是 7 天,一个窗口里塞了三个促销活动,模型会把增量混在一起,学出来的门控分辨率会很差。内部经验是一次尽量落在单一事件的影响范围内。
4.2 损失函数的三个组成部分
事件感知模型的损失不能只用一个 MSE。我目前在用的组合有三块。
- 基础预测损失:所有时间步上的主流损失,可以是 MSE 或分位数损失。
- 事件窗口加权损失:只对事件窗口内的时间步额外加权,让模型优先修正事件时段。
- 增量平滑损失:对 delta 做时间差分正则,防止事件增量在窗口内剧烈震荡。
如果你做的是区间预测,把分位数损失加进事件窗口同样适用。事件窗口内如果预测区间覆盖不足,调整权重比调整网络结构更有效。
4.3 别被全时段 RMSE 骗了:分段回测与事件窗口指标
这是我想单独强调的一点。非平稳数据上,只看整体 RMSE 是危险的,因为事件样本占比小,误差会被平均掉。我现在的评估协议是分段回测:把测试时间段切成长度固定的若干个窗口,每个窗口单独计算指标,最后分三组报告。
| 指标 | 全时段 | 非事件时段 | 事件时段 |
|---|---|---|---|
| RMSE | 0.87 | 0.73 | 1.42 |
| MAE | 0.66 | 0.55 | 1.10 |
| P90 Coverage | 0.84 | 0.86 | 0.76 |
如果不单独看事件时段的误差,你会以为模型已经很好,实际上在事件窗口内完全是另一番景象。我建议把“事件时段误差 / 非事件时段误差”作为模型上线的硬性门槛,这个比值超过 2 就要警惕,说明事件感知分支基本没起作用。
5. 上线之后真正的难点:事件流质量、冷启动与兜底
5.1 区分计划事件与实时事件
线上推断和离线训练不一样,事件信息不是“全知”的。运营排期里的活动,系统可能提前三天就知道;气象预警和突发舆情,可能只能提前几小时甚至实时才知道。如果模型把“未来事件”的整体信息和“当前已知信息”混在一起,很容易在特征上造成数据泄漏。
我的做法是给事件流打两个标签:确定性事件和预警事件。确定性事件提前进入特征,预警事件只在发生时进入,并且专门加一个“信息状态”特征,告诉模型当前是处在“已知事件即将发生”的状态,还是“事件突然被发现”的状态。这个细节能明显提高预测在事件前兆阶段的稳定性。
5.2 事件标注噪声的应对
事件标注质量是线上最大的隐患。我踩过的典型问题包括:时间偏移半天、类型标错、强度等级主观性太强。解决思路是在事件管理端维护一个置信度分数,在特征化时把置信度乘进门控置信度上。低置信度事件照样可以触发,但模型不会完全信任它,相当于给事件信息加了一层贝叶斯先验。
5.3 新类型事件的冷启动与人工增量通道
模型没见过的新事件类型,随机初始化的 embedding 基本等于噪声。处理办法是退化到粗粒度事件类型:把相似的事件类型聚合成大类,新事件自动映射到大类上,再配合规则兜底。对于极大影响的事件,比如区域性的突发极端情况,我会在系统里留一个人工增量通道,让运营同学可以传入人工估计的修正量。这个增量不是简单加到最终预测上,而是作为事件分支 delta 的修正约束,模型在窗口内还会自己做平滑。
做完这一套之后,我最深的体会是:预测不是在和噪声斗争,而是在和信息不完备斗争。所谓非平稳,很多时候只是因为有一类关键信息——事件——没有被显式表达出来。如果你也遇到“忠实复现历史却预测不了突变”的情况,可以先别急着换更复杂的模型,把事件信息规范化地喂进去,效果往往比换十个架构都明显。