时序异常检测:断点、窗口模型与预测误差
什么是时序异常检测?
时序异常检测(Time Series Anomaly Detection)是从按时间顺序排列的数据点中,识别出不符合预期模式、波峰、突变或趋势中断的观测值的过程。这些异常可能代表系统故障、欺诈行为、传感器漂移或真实的事件信号。本教程聚焦三种核心思路:断点检测、窗口模型与预测误差,帮助初学者理解原理并能应用于实际数据。
异常类型的快速划分
在深入方法前,先建立对异常形态的基本认知。常见异常可归为两类:
- 点异常(Point Anomaly):单个时间点显著偏离邻近值或全局分布。例如,温度传感器瞬间跳变到200°C。
- 模式异常(Pattern/Collective Anomaly):一段子序列的整体形状与历史模式不符,但其中的单个点可能正常。例如,心电图出现一段反常的平坦曲线。
断点检测擅长发现模式的突然变化,窗口模型能捕捉局部上下文偏离,而预测误差方法则对点异常及模式偏离均有效。
断点检测:捕捉时间序列的结构性变化
断点(Change Point)是指时间序列的统计特性(均值、方差、趋势等)发生突然改变的时刻。检测断点是理解序列演化的关键,许多异常正是由结构突变引起的。
核心思想:寻找分布不一致的分割点
给定序列 ( x_1, x_2, \dots, x_T ),断点检测算法试图找到若干位置 ( \tau ),使得分割出的各段内数据服从相似的分布,而相邻段间差异最大。
常用方法:PELT 算法
PELT(Pruned Exact Linear Time)是一种高效的离线断点检测算法,时间复杂度为 ( O(n) ),基于代价函数和惩罚项:
- 代价函数:衡量段的内部一致性,通常用负对数似然,如对均值变化采用二次代价 ( C(x_{a:b}) = (b-a+1) \log \hat{\sigma}^2 )。
- 惩罚项:控制断点数量,防止过分割。常见惩罚为 ( \beta \times \text{(断点数)} ),( \beta ) 通常采用 BIC 或 MBIC 准则。
PELT 通过动态规划递归计算,并利用剪枝技术加速。在 Python 中可直接使用 ruptures 库:
import ruptures as rpt
model = rpt.Pelt(model="rbf").fit(signal)
breakpoints = model.predict(pen=10)
参数选择与注意事项
- “rbf” 模型适用于均值偏移的断点,“l1” 或 “l2” 分别针对中位数和均值变化。
- 惩罚值
pen越大,检出的断点越少。可先参考pen = log(n)或根据领域知识微调。 - 断点检测对噪声敏感,建议先进行平滑(如移动平均)或使用稳健的代价函数。
窗口模型:基于局部上下文判断异常
窗口模型不假设全局分布,而是围绕每个点取一个邻域窗口,通过与窗口内的统计特征比较来判定异常。这类方法天然适应非平稳序列中的局部异常。
滑动窗口与局部异常分数
最简单的窗口模型是滑动 Z-score:对每个点 ( x_t ),计算其相对附近窗口均值和标准差的偏差:
- 定义窗口大小 ( w )(如前后各 ( k ) 个点,或仅用过去窗口避免未来数据泄露)。
- 计算局部均值 ( \mu_t ) 和标准差 ( \sigma_t )。
- Z-score = ( (x_t - \mu_t)/\sigma_t )。
- 若 |Z-score| > 阈值(如3),则标记为异常。
但该方法易受窗口内已有异常影响(掩蔽效应)。改进方案使用**中位数和中位绝对偏差(MAD)**代替均值和标准差,更稳健:
import numpy as np
from scipy.stats import median_abs_deviation
def robust_zscore(point, window):
med = np.median(window)
mad = median_abs_deviation(window, scale='normal')
return 0.6745 * (point - med) / mad
基于窗口的判别模型:Discord 发现
时序 Discord 是指与所有其他相同长度子序列最不相似的子序列。它捕获的是形状上最异常的模式,非常适合模式异常检测。算法通过计算两两子序列间的距离(如欧氏距离),并采用 SAX 或基于矩阵概要(Matrix Profile)等技术大幅加速。
矩阵概要方法能同时给出每个子序列与其最近邻居的距离,距离峰值对应 discord。stumpy 库实现了高效计算:
import stumpy
mp = stumpy.stump(ts, m=window_size)
anomaly_idx = np.argmax(mp[:, 0])
预测误差方法:当预测值与实际值分道扬镳
基于预测的方法遵循一个直观原则:若时间序列在某一时刻的预测误差突然增大,说明该点偏离了正常演化轨迹,很可能是一个异常。
构建预测模型作为“正常行为”基准
任何能够合理拟合历史数据的模型都可用作正常行为的生成器。常用模型包括:
- 简单模型:移动平均、指数平滑(Holt-Winters)、季节性分解(STL)。
- 传统时序模型:ARIMA、SARIMA。
- 机器学习模型:随机森林回归、XGBoost(需谨慎处理时间依赖)。
- 深度学习模型:LSTM、CNN、Transformer——擅长捕获复杂非线性模式。
无论选择哪种模型,核心流程均为:
- 训练:用“干净”或包含少量异常的历史数据训练模型。
- 预测:在时间 ( t ),基于可用历史预测 ( \hat{x}_t )。
- 计算残差:( e_t = x_t - \hat{x}_t )。
- 阈值判定:对残差序列应用异常检测(如固定阈值、残差的 Z-score 或动态阈值)。
动态阈值与残差建模
简单的3-sigma方法在残差非正态时失效。更好的做法是对残差本身建模:
- 残差的滑动统计:对残差同样使用滑动 Z-score 或移动中位数+MAD,自适应变化。
- 残差的预测区间:使用分位数回归或 bootstrap 生成预测区间,实际值落在区间外即异常。例如,Facebook Prophet 的
yhat_lower和yhat_upper直接提供不确定性区间。
示例(Prophet):
from fbprophet import Prophet
df['cap'] = df['y'].max() + 1 # logistic 增长需设上限
model = Prophet(interval_width=0.99, changepoint_prior_scale=0.05)
model.fit(df)
forecast = model.predict(future)
anomalies = df[df['y'] > forecast['yhat_upper']]
预测误差方法的陷阱
- 异常污染训练集:若训练数据包含大量未标注的异常,模型会学到错误的模式。解决方法:先用简单阈值过滤极端值,或采用稳健回归。
- 错误累积:在回归模型中,若每一步用实际值作为输入进行下一步预测,会掩盖异常。应采用一步预测滚动的模式。
- 概念漂移:当序列的正常模式缓慢变化时,需定期重新训练模型或使用在线学习方案。
三种方法的对比与组合策略
| 方法 | 擅长场景 | 典型输出 | 局限 |
|---|---|---|---|
| 断点检测 | 检测整体统计特性突变,如工艺参数调整、市场模式变化 | 断点位置集合 | 无法识别短暂的点异常 |
| 窗口模型 | 局部上下文异常,模式反常,且对非平稳序列友好 | 每个点的异常分数或标签 | 窗口大小敏感,计算代价可能较高 |
| 预测误差 | 点异常、微小偏离,可结合趋势与季节 | 残差与阈值边界 | 依赖模型假设,对训练数据质量敏感 |
实际工作中,常组合使用以获得更完善的检测效果:
- 先用断点检测将序列切分为相对同质的区间,然后在各区间内应用窗口模型或预测误差方法,避免跨模式误报。
- 使用预测误差方法提供的残差作为窗口模型的输入,叠加异常判定规则。
- 对多模型结果进行投票或加权,提升鲁棒性。
动手实践建议与工具链
- 数据探索:画图,观察趋势、周期、噪声水平。标记肉眼可见的异常作为评估基线。
- 库选择:
ruptures:丰富的断点检测算法。stumpy:高性能的模式异常检测(Matrix Profile)。prophet:带趋势、季节和假期效应的预测模型,天然输出不确定性区间。PyOD、scikit-learn提供通用异常检测器,可结合时间特征使用。
- 评估:若缺乏标签,可使用领域反馈迭代验证;有标签时采用时序交叉验证(避免未来信息泄露),并关注 precision、recall 与时序上的延迟容忍度。
时序异常检测没有万能银弹,理解数据特性、结合多种视角进行判断,是通向可靠检测的必经之路。