从零开始构建业务异常检测系统,FreeWheel面临过的问题和解决方案
在公司运行过程中,尤其是对于偏重数据的互联网公司,业务异常检测是一个非常重要但又很容易被轻视的工作。一旦因为业务发生异常并且没有被及时发现,一定会对公司和客户产生某种程度的损失,从而影响业务正常发展。很多公司都构建了基于规则的报警平台,并将其应用于业务的异常检测。但由于数据模式的快速变化,并且数据中存在着大量噪音,基于规则的异常检测误报率较高。基于机器学习和人工智能的业务异常检测可以获得比传统规则系统更高的准确率和扩展性,但由于面临诸如异常的定义较为模糊、缺少数据标签等诸多挑战,构建一个实用的业务异常检测系统需要算法工程师和数据开发工程师的精雕细琢才能完成。
FreeWheel 是一家数字广告管理技术和服务提供商,创建于 2007 年,是一家专门提供电视及互联网视频广告投放、监测、预测、增值等关键解决方案的技术公司,主营业务为高端视频媒体的广告服务。为客户提供稳定可靠的广告投放服务是 FreeWheel 的宗旨,为了提高服务质量,对业务的异常检测和预警非常关键。我们从 2020 年开始从零打造了基于机器学习的业务异常检测系统,覆盖了 FreeWheel 核心业务指标,为客户的广告投放保驾护航。
本文介绍了 FreeWheel 基于机器学习的业务异常检测实践,提炼了从零开始构建业务异常检测系统面临的问题和解决方案,文章介绍了常用的异常检测算法,比较了不同算法模型的优劣,介绍了可扩展的异常检测系统是如何搭建的,希望对于从事相关工作的朋友能够带来帮助。
从应用场景分,异常检测包括指标异常检测、日志异常检测、网络异常检测、用户行为异常检测(风控、反作弊)等。从数据角度又可以分为新奇点检测 (Novelty Detection) 和离群点检测 (Outlier Detection)。
广告业务异常检测是以业务指标为基础,多维度、多角度的异常检测。一方面,业务指标是连续采集的时间序列,通常认为历史序列是正常的,从中学习特定的模式,未来的指标如果违反了这个模式,可以认为出现了异常,从这个角度看,属于新奇点检测的范畴。另一方面,业务指标在某几个维度下的值一般会满足某种分布,和多数正常值相比差别比较大的可能就是异常点,这属于离群点检测的范畴。受篇幅所限,本文主要关注新奇点检测问题,离群点检测的相关实践将在后续分享。
异常检测面临诸多挑战,第一,正常和异常的边界是非常模糊的,很多时候是“公说公有理,婆说婆有理”;第二,历史数据里经常包括很多噪音,甚至历史数据中就存在着异常。第三,几乎没有标注的异常标签;第四,正常数据的模式也不是一成不变的,会随着时间的推移和业务的演进发生很大的变化。指标异常检测算法,包括无监督、半监督、监督机器学习算法。其中以无监督应用最为广泛。无监督主要有基于统计模型的异常检测(如 EVT),基于时间序列预测的异常检测,基于隐变量重构误差的异常检测(如 VAE),以及其他深度学习的衍生模型(如 AnomalyTransformer) 等。监督的算法更偏向于传统的机器学习和深度学习的分类模型,半监督方法致力于解决标签数据不足的问题,提高监督方法的学习效果。本文主要介绍我们从零到一的实践经验和在生产环境中应用较为成熟的算法模型,除此之外,基于监督和半监督方法的模型也很快投入生产环境,期待后面分享给大家。
FreeWheel 监控平台目前有两大指标数据来源,Prometheus 和数据平台。Prometheus 是目前主流的开源监控解决方案,具有数据模型灵活、延迟低等特点,目前存储 FreeWheel 绝大多数系统和应用指标和核心业务指标。Prometheus 的核心业务指标是从 Ad Server 直接采集而来,如请求量、广告曝光量等,聚合粒度较粗。另一部分来源于 FreeWheel 数据平台,通过对 AdServer 产生的广告日志的实时处理,将更细粒度的指标存入 Druid,其数据延迟为分钟级。有了细粒度的数据后,我们可以基于我们关心的维度进行实时聚合,如广告曝光量这个指标,根据客户 ID 进行聚合,可以对每个客户 ID 的广告曝光量时间序列进行异常检测和报警。
最早的时候,我们通过规则的方式对几个关键的指标配置了预警,比如,某个重要客户的曝光量或者转化率小于某个阈值就会触发报警;比如针对所有的站点,若流量同环比小于 80%,就认为可能有问题。这些规则确实起到了一定的作用,也检测到了很多异常,但缺点也很明显:第一是规则太多,不同客户的业务和流量模式千差万别,同一套阈值比较难以满足需求;第二点是业务变化很快,阈值需要随着业务变化不断地调整,维护成本高;第三点,也是最大的痛点是误报率大,需要耗费大量的人工处理报警。
为了从零开始快速搭建起基于机器学习的 V1.0 的异常检测系统,我们采用了简单直接的做法,基于历史数据训练回归模型,预测未来的指标和对应的上下界,如果真实值超过了上下界,则检测到异常并触发报警,也就是基于时间序列预测的异常检测。
(注:绿色实线时指标的真实值,蓝色虚线是指标的预测值,橙色和红色分别是上下届,红框标注的是检测到的异常)
首先,为了能快速上线第一个版本,我们尝试了时间序列预测里最常用的 ARIMA 模型,ARIMA 是 Autoregressive Integrated Moving Average model 的缩写。ARIMA 模型有三个超参数 p,d,q,一般写作 ARIMA(p,d,q) 中,AR 是“自回归”,p 为自回归项数;MA 为“滑动平均”,q 为滑动平均项数,d 为使之成为平稳序列所做的差分阶数。对于每一个时间序列,都需要确定最适合的超参数 p,d,q,通常有一套成熟的策略进行人工选择,如通过观察差分之后的平稳性确定 d,通过观察 ACF 曲线和 PACF 曲线确定 q 和 p 等等。但是,对于数以万计的时间序列来说,人工调整显然不可行,这套经验策略也较难量化,因此我们采用类似网格搜索的方法确定超参数,这一过程也被称为自动定阶。
ARIMA 模型没有考虑时间序列的季节性(也称作周期性,下文统称周期性)变化,但周期性是大多数跟流量相关的指标必须要考虑的一个因素,并且不同的业务模式的流量周期是不一样的,看下面的两个例子。
如上图所示,按小时粒度聚合的 2 个指标,其周期分别为 24 和 168,分别代表每天重复的模式和每周重复的模式。
我们需要给出最匹配这个时间序列的周期值,即周期性检测。周期性检测的方法有很多,第一种就是相似度检测,假设周期为 T,将时间序列按照长度为 T 进行切分得到若干个分段,计算相邻分段的相似度。第二种方法是分析 ACF 曲线,ACF 全称是 Autocorrelation function,其表达了时间序列和自身偏移一定量之后的相关性。通过观察 ACF 曲线的特点可以推断出时间序列周期。除了这两种方法外,周期性检测还有快速傅里叶变换(FFT)、小波变换等方法。我们根据不同指标的时间粒度、数据量和不同方法的计算复杂度、准确性等,根据经验和实验结果构建了一个的选择周期性检测方法的决策树,由最适合的一种或几种方法,综合计算出时间序列的周期。
由于普通的 ARIMA 模型不能够处理周期性,Seasonal ARIMA 模型引入了季节分量,可以更好地处理周期性,通常写作 SARIMA(p,d,q)(P,D,Q,s) 。s 是时间序列的周期,P,D,Q 分别对应季节分量的滑动平均项数、差分阶数和滑动平均项数。
在训练数据的选取上,虽然历史数据越多,模型拟合地会更好,但并不是越多越好,一方面,数据量增加会使得 ARIMA 模型拟合时间变长,另一方面,业务指标的模式可能随着时间而发生变化。以小时粒度指标,对于无周期的 ARIMA 模型,200~300 多个点即可取得比较好的效果,也就是 7~14 天左右的历史数据;对于周期性的 SARIMA 模型,5~10 个周期即可取得比较好的效果,对于呈现每周重复的模式,6 周左右的数据可以取得比较好的效果。
有了预测值之后,接下来我们需要得到判断异常的上下界阈值,ARIMA 模型在输出预测结果的同时,也输出了置信区间。置信区间概率论里的一个概念,是基于区间估计的结果,在预测的场景下,代表预测结果会一定的概率出现在这个区间,这个概率就被称为置信度。当随机变量符合正态分布时,95% 置信度的置信区间近似等于均值加减 2 倍标准差,而均值加减 3 倍标准差的置信度为 99.7%,这也就是常说的 2 倍标准差法和 3 倍标准差法。将置信区间作为判断异常的上下界阈值时最适合不过的了,当置信度越大时,置信区间越宽,超出上下界阈值的异常就越显著,换句话说,业务指标的异常就越严重。通过设置不同的置信度,我们可以探测到不同严重程度的异常。
在实际应用时,由于我们的业务指标通常时非负的,并不能满足正态分布(或者高斯分布),因此 ARIMA 模型直接输出的置信区间就不合适了。通过分析发现,绝大多数业务指标近似满足从零处截断的截断正态分布(高斯分布),因此我们只需要取出 ARIMA 模型输出的预测值和标准误差,就可以利用截断正态分布的累计分布函数和分位函数计算出置信区间。
有了第一版的模型的结果,我们上线了异常检测系统 V1.0,架构如下图所示:
V1.0 系统的核心就是指标预测服务,指标预测服务将需要预测的指标的预测结果和报警上下界输出到监控平台,通过在监控平台上对需要进行异常检测到指标配置报警规则,由监控平台实时检查是否满足报警要求,也就是超过上下界,从而触发报警。指标预测服务是基于 PySpark 实现的,由两类定时任务构成。第一类是周期性检测和模型训练,频率较低,大约每天执行一次,负责将所有需要检测的指标和维度下的时间序列都进行周期性检测、ARIMA 超参数选取和 ARIMA 模型训练,并将周期数、ARIMA 超参数和 ARIMA 模型参数进行保存。
第二类是预测任务,基于 Spark 的并行计算能力,可以实现在较短时间能完成大量时间序列的预测工作。以小时级任务为例,每次预测任务都会预测时间序列未来 N 个小时(例如 N=24)的指标值和上下界,并写入数据库;小时级任务可以配置每隔 M 个小时(例如 M=6)执行一次,同时覆盖之前的预测结果。这样做的好处是既保证了预测数据的冗余,使得在预测任务失败或者延误的时候还有之前的预测结果可以使用,同时执行预测任务的 executor 可以允许在按需申请的 spot instance ec2 上,节约了计算成本,由于 spot instance 的不可靠性,预测任务可能随时失败,只要重试即可。另一方面,配置 M<N,可以更好地利用最新的数据,捕捉指标的变化,提高预测结果的准确度。
对第一版的结果进行分析,我们发现有下面几个问题会导致精确率(Precision)不足,误报较多:
1) 区间太紧
首先是置信区间的问题,当我们选好一个合适的置信度,如 99%,会发现对于多数时间序列都是合适的,但对于那些规律性比较强的时间序列来说,其模型的拟合度非常好,MAPE 能小于 5%,预测的标准误差很低,因此置信区间会比较窄,如图:
这就会导致如果指标出现轻微的抖动,比如 +/-10%,就会被识别为异常,这显然是我们不希望看到的。我们的改进有两点,第一是对标准误差进行放大,当标准误差 / 均值的比例小于一定的程度时,将标准误差乘以一定的放大系数,再计算置信区间;第二是利用预期的抖动比例进行干预,将计算出来的置信区间和基于经验配置的容忍抖动比例(如 +/-20%)进行融合,这样得出的上下界既能符合模型的拟合结果,也能在业务上看来不至于特别离谱。
2) 数据包含噪音
第二点,历史数据中包含异常点,会对模型的拟合和预测产生一定的影响。例如如果前一天流量因为某些原因(如压力测试)有一个很明显的尖峰,那大概率 ARIMA 模型预测的今天同周期也会相应地变高,从而导致对正常流量的误判。我们从两个方面解决这个问题,第一是模型启动的时候,我们用一个规则去识别那些比较明显的异常点;然后,当我们的模型开始运行,异常点被检测出来后,我们通过建立反馈机制修正模型的输入数据,将异常点的值修正为此前的预测值,后面模型的预测将不会收异常点的影响。当然如果异常点识别错了,反馈机制会带来负面效应,处理这个异常报警的运营人员会对其进行标记,从而避免这个问题。
3) 数据太稀疏
此外,我们发现,有一大部分时间序列的数据非常稀疏,也就是其历史上的取值经常缺失,导致时序预测准确度较差。针对这种情况,从业务的角度考虑,我们通过设置阈值跳过数据太稀疏或者历史流量过少的的场景,减少误报。
V1.0 异常检测系统有几个问题,首先在系统层面,随着需要配置异常检测报警的指标越来越多,通过 Hard Code 的方式部署的指标预测服务的扩展性问题就凸显出来;另外,业务上希望对于指标短时间抖动或者业务影响比较小的异常进行过滤,现有的架构难以实现。在模型层面,ARIMA(SARIMA)模型在很多场景下预测误差较大,基于这样的预测结果计算的上下界会导致较多误报警。
针对 V1.0 系统和模型的不足,我们设计了第二代异常检测系统 V2.0。
在第二版的异常检测系统中,我们将异常检测的工作从监控平台完全剥离出来,专注于优化异常检测算法和策略,进而提升异常检测的效果。异常检测系统将结果以异常得分的形式输出给监控平台,由监控平台负责报警和运营操作。下面是系统整体的架构图。
异常检测系统包括元数据管理、模型训练、异常评估等几个模块。元数据理负责和监控平台同步异常检测需求和配置信息,如要检测的指标、数据源、维度、过滤条件等,并生成对应的时间序列元数据。模型训练和之前相似,不同的是从批任务变成了实时任务,通过内置的调度模块,一方面要服务监控平台实时配置的需求,对于新增的时间序列要优先进行训练,另一方面也要定期地对旧模型进行更新。
异常评估模块也是一个长期运行的 Spark 应用,内置的调度模块会调度每个任务的运行,同时考虑实时数据源依赖、数据完整性检查、指标历史数据缓存、任务优先级等,将适合的任务提交 Spark Job Group。每个 Spark Job Group 都包括指标数据查询、数据处理、并行的时间序列异常检测和结果汇总与输出等多个 Spark Job/Stage,其中最核心的是并行的时间序列异常检测的 Stage,多个 Task 由 Spark 调度并行执行。
异常评估模块的另外一个关键点是对异常进行评估和打分 (0 到 1 之间的分数),异常比较明显或者对业务影响比较大的异常的得分更接近 1,不明显的异常、噪音、对业务影响小的异常的得分更接近 0。相比 V1.0 异常检测系统,引入异常评估模块后极大地提升了异常检测的能力,一方面可以引入基于规则和策略的评估,另一方面可以直接基于无监督或者监督的机器学习模型给出异常打分。由于基于规则和策略的评估方法可解释性更强,占线上多数场景都采用此方法;在一些特殊业务场景中,通过模型直接打分也取得了不错的效果。
下面简单介绍下我们的打分策略,首先,选取评估窗口,即同时评估最近的几个时刻指标值的异常情况,评估窗口数据的异常相比只评估点数据的异常可以减少噪音的影响,当然,越新的数据权重越大。以流量指标为例,我们综合了以下几个因素进行评估:是否超出上下界、相比预测值流量异常下降(或者上涨)的幅度,即重构误差、历史流量大小,当前所处时刻流量的相对大小、异常持续的时间、历史上发生过类似异常的频率等。
除了基于影响的评估之外,我们还构建了根因分析系统对异常进行归因分析,提供一站式业务运营解决方案,极大地提高了运营效率,同时消除由于业务上预期的“指标异常”导致的误报警。对于可以简单找到根因的异常,我们选择直接在异常检测阶段进行消除,而不是导入根因分析系统,来减少计算压力。例如,程序化交易的流量异常下降,很可能是交易暂停,或者已经达到预算等原因导致的。
ARIMA(SARIMA)模型能够较好地拟合大多数的时间序列,但在实际使用中有两个比较突出的问题:一、对于不带周期项的 ARIMA,其预测结果会有较为明显的滞后现象,容易导致误判;二、如果周期数过大,模型拟合的速度很慢,如对于小时粒度的数据,当周期为 168 时,其单线程拟合时间超过 5 分钟。为了弥补 ARIMA 模型的不足,我们引入了 XGBoost、STL、SMA 和 EVT 等模型,不同的模型有各自的优缺点和适合的应用场景,下面我们先简单介绍下这些模型。
XGBoost
XGBoost 是一个梯度提升决策树(GBDT)的高效实现,以极强的模型学习效果和性能著称,可以解决 SARIMA 长周期预测性能无法满足要求的问题。应用到时间序列预测时,需要人工进行特征工程,我们选取了这么几类特征:
第一类是时间序列的 lag(滞后算子),也就是要预测的时刻 T 的前面时刻的值,如 T-1,T-2…. lag 并不是越多越好,对于小时粒度的数据,如果输入是 5 周(840),那么约靠前的 lag 可能的价值就越低,我们根据周期的不同,选取的 lag 数也不一样。如前序判断为 24 周期,则选取 1~40 lag,和每 24 个周期选取同周期的 lag,如 t-48,t-96,等等。
第二类是时间特征,如星期几、是否为周末、当前小时等。
第三类是时间窗口统计特征,如最近 24 小时的平均值,工作日的平均值,周末的平均值,过去 7 天当前小时的平均值,等等。
XGBoost 的拟合能力是非常强的,因此摆在我们面前很大的问题是如何避免过拟合,也就是虽然在训练数据上模型拟合地非常好,但在验证数据上预测误差较大。首先是从参数入手,包括使用 L2 正则,限制树的深度、对训练数据进行采样,预剪枝等参数都会起到一定的效果。此外,如果训练数据不足,过拟合是很难避免的,因此 XGBoost 只适用于时间序列历史数据非常多的情况。
此外,另一种解决单一的时间序列训练数据量较少的方法,是通过对相同指标不同维度的时间序列进行聚类,将相似的维度值对应的时间序列放到一起训练模型,这样可以增加训练数据量,缓解过拟合的问题。但这种方法扩展性较差,需要根据具体的指标和维度对应的时间序列的情况单独调整。
STL
对于某些长周期的指标,我们面临 SARIMA 拟合时间非常长,又没有足够的数据训练 XGboost 的情况,这时候时间序列分解派上用场了。时间序列分解是将时间序列分解为均值 (Mean) 、趋势 (Trend) 、季节 (Seasonality) 、循环 (Cycle)、随机误差 (Random) 这几个部分,分解方式通常包括乘法和加法。趋势表示这个时间序列的长期趋势,通常加上了均值,季节性(也叫周期性)指时间序列随着时间的季节波动,通常是年、周、日等,循环指的是指标在较长时间呈现出上下波动,通常会被合并到趋势项中,称作趋势 - 循环项(trend-cycle)。
将时间序列的周期和趋势分解开之后,我们可以通过更加简单的模型,如 ARMA,去拟合趋势,对于周期项,只需要简单的重复即可,最后将趋势的预测结果和周期相加即刻得到最终的预测结果。
图片来源于https://otexts.com/fpp2/stl.html
经典的乘法和加法分解只能支持固定周期,且受历史数据的异常点影响较大,STL(Seasonal and Trend decomposition using Loess) 分解则较好地解决了这两种问题,我们选用 STL 分解结合 ARIMA 拟合趋势的方法,较好地解决了长周期时间序列的预测问题。除了 STL 之外,还有 X11 和 SEATS 等分解方法。
SMA
在线上实际运行时,我们发现无论是 ARIMA、XGboost,还是 STL 分解,其模型训练时间都在分钟级,预测时间都在秒级,对于那些时间序列数量巨大的业务指标来说,显然是非常不经济的。
同时我们发现,这些业务指标都有一个特点,对于多数时间序列,他们的模式是非常稳定的,因此设计一种快速地算法解决这类问题可以极大地降低异常检测的成本。
我们从常用的同环比出发,设计一种结合周期的带权移动平均方法(Seasonality Moving Average,简称 SMA),可以在毫秒级完成预测任务。预测值为同比(最近几个周期同时刻)和环比(最近的数据)的加权平均,以小时粒度指标,周期为 24 时为例:
r(recent): 考虑最近的数据点数
α(alpha): 最近数据的权重
c(cycles): 考虑的周期数
o(offsets): 同周期前后偏离的点数
对于以上的时间序列预测模型,需要评估其预测的准确程度,我们选用 SMAPE 作为预测模型准确性的评估指标:
SMAPE 反映了模型拟合历史数据的误差,在模型拟合能力一定时,也反应了该时间序列的可预测性,或者叫模式(pattern)的强弱。上面四种模型基本上可以满足多数时间序列的异常检测,但是对于周期性和模式都比较弱的时间序列来说,上述模型预测误差都比较大,通常 SMAPE>70,会导致有较多的异常误报。最初的做法是忽略这类时间序列的异常检测,在一定程度上解决了异常检测精准率不足的问题,但也降低了召回率。
为了提升周期性和模式都比较弱的时间序列异常检测的召回率,我们引入了极值理论(Extreme Value Theory,简称 EVT)。极值理论可以在数据分布位置的条件下,估计极值(极大值和极小值)的分布,从而估计正常数据合理的上下界。由于篇幅原因,本文不详细描述具体算法实现,大家感兴趣可以阅读论文(https://www.researchgate.net/publication/318919520)。论文作者考虑了概念漂移的问题,但没有考虑数据的周期性。对于存在一定周期性的时间序列,我们在应用极值理论模型的时候会先根据周期性进行数据采样,对于同周期的数据,采样的概率更大,这样更符合实际情况,对异常检测的准确率也更高。
目前我们引入的模型有 ARIMA、XGBoost、STL-ARIMA、SMA、EVT 等,我们先来总结一下这些模型的特点:
随着模型引入越来越多,我们需要一套方法为特定的时间序列选择合适的模型。根据这些模型的特点,我们建立了一个决策树,根据指标的类型、周期性、历史数据量、实时性、成本等因素,选择合适的候选模型方案。
一个候选模型方案包括首选模型、若干个备选模型和保底模型。举一个典型的例子,因为极低的成本,SMA 将被作为首选模型,首先用 SMA 拟合时间序列的历史数据,并给予设定好的验证数据窗口,如最近 3 天,计算预测的 SMAPE 误差,若 SMAPE 小于预设的阈值(如 50),则认为 SMA 的拟合是有效的并将其作为模型选择的结果,否则,将尝试备选模型。假设 ARIMA、XGBoost、STL-ARIMA 都可以做为备选模型,则分别尝试对这三种模型进行拟合,在 SMAPE 小于阈值的模型中选择最优的作为模型选择的结果。如果备选模型都不能满足需求,则判断有没有保底模型,如果有保底模型(比如 EVT),则将其作为模型选择的结果,否则这个时间序列被认为是无效的。
FreeWheel 业务异常检测系统从上线至今已有两年的时间,共接入了十几种业务场景和几十种指标,如针对不同客户集成方式的流量监控、程序化交易全生命周期的异常检测及根因分析等,帮助 FreeWheel 和客户主动发现广告投放中的若干个 P1、P2 级别的严重问题,减少了客户的损失,在维护客户关系方面发挥了重大作用。下一步,我们会支持更多的业务,尤其是针对 FreeWheel Marketplace 的业务,发现和解决广告需求侧和供给侧之间的匹配问题,以及客户流量的广告填充率不足的根因分析,帮助客户提高广告投放效果和利润。
从算法和模型的角度,目前线上大多数模型都是基于时间序列预测、针对特定指标和维度(时间序列)自动训练的小模型,其优点是灵活性和扩展性好,成本低;缺点也比较明显,不方便针对具体的业务优化特征工程,包括多指标协同、不同维度和标签之间的数据依赖等。因此,我们针对几种特殊的业务场景,如程序化交易,开发了基于神经网络的大模型;除此之外,我们也对其他的无监督、监督和半监督方法进行了研究和开发,希望后面能分享给大家。
钟雨,本科和研究生就读于清华大学,现任 FreeWheel 异常检测团队主任算法工程师,FreeWheel 业务异常检测算法团队负责人。曾供职于京东广告数据团队,Spark Contributor,具备丰富的大数据开发与调优、数据挖掘和机器学习经验,在广告大数据行业深耕多年。
你也「在看」吗? 👇
微信扫码关注该文公众号作者