前言,我们认为智能技术会赋逐渐能所有的传统行业,利用数据和技术优化效率,提升体验,创新模式。审计是一个非常传统的业务领域,也是一个非常依赖数据和信息的领域,我们在审计领域的实践在探索,如何将异常检测算法在审计中应用。
背景
引用MBA智库百科的原文:内部审计,是建立于组织内部、服务于管理部门的一种独立的检查、监督和评价活动,它既可用于对内部牵制制度的充分性和有效性进行检查、监督和评价,又可用于对会计及相关信息的真实、合法、完整,对资产的安全、完整,对企业自身经营业绩、经营合规性进行检查、监督和评价。
通俗一点讲,传统的内部审计就是要从内部发现企业运营中是否有风险、隐患,能够提前识别风险或者在风险发生的时候给出预警,将对企业的运营有着很好的正向作用。
那么说到识别风险,屏幕前的你如果有算法相关知识,肯定能猜到这类问题隶属于什么算法范畴;如果没有相关的知识也无妨,让我们来给你一些提示,这项技术在我们生活中经常会使用到,比如说我们平时上市场买水果,总会把篮子里坏的水果剔除出去,这就是这项技术的简单实现啦~这项技术就是异常检测(Outlier Detection),是机器学习算法里面的一个分支。成熟的异常检测算法和理论在网上一抓就能抓到一大把,但是在实际的商业、生产环境,真的什么算法都适用吗?
传统金融行业审计问题预警的难点
传统金融行业的审计案例记录的比较少,甚至我们做这个项目半年多的时间只见到过不超过20个案例,这些案例的是完全不足以作为Label支撑监督学习的。所以我们的算法大多数都是基于无监督学习的,无监督学习意味着算法模型的有效性没有办法直观的衡量,任何发现的审计问题预警都需要相当长时间去证明,这些预警里面有些是真的审计风险,而大部分肯定只是一些比较特殊的正常情况。
在AI风口上,各个企业搞什么都喜欢用 NN(神经网络),左一个NN右一个NN,NN就像魔法一样突然解决了很多人们之前没办法解决的机器学习问题。但对不起,敏感的传统金融行业往往会拒绝这个魔法,甚至拒绝所有他们一下子理解不来的算法。
你可能会觉得这些传统行业真的很死板,我个人也是这样觉得的,但是涉及到审计问题,这些黑魔法或许还真的不适用。举个例子,如果有一天总行审计部找到另外一个部门说其有审计风险,但是又说不出为什么有风险来,只是机器告诉他们有风险,需要处罚该部门,岂不是让人笑掉了大牙。
所以说,涉及到审计问题的算法,不仅要高效、还要简单,让人一下就能说出预警的到底是什么风险,毕竟现阶段,做决策的主体还是人。
"永远都有比你想象中更差的数据。",这句话源于我的一位同事,其已经接触过了很多数据类的项目,总结出来独了这样一条"真理"。新兴行业中咱就不说了,谁能想到传统行业的数据也是脏乱差呢?我刚来到项目上接触到数据的时候也头疼不已,原来安排的工作量都被环境和数据给拖垮了。所以说,做数据类项目,你永远要保持对数据一颗"畏惧"的心,你不要想着那么容易就可以征服它。留够足够的buffer,先检查数据的完整性和可用性是很有必要的。
数据
说了那么多背景知识,让我们把焦点放到技术上吧~孙子有云:「知彼知己,百战不殆。」而在数据项目中,我们的"彼"就是可以利用是数据,只有了解数据才能更好的完成我们的算法任务。
我们能接触到的原始数据,其实已经是经过一次汇总的了,准确的来说是每一笔业务发生后汇总上来的数据,时间颗粒度是到月。
比如最底层的数据可能是(因为我们接触不到,只能大概推测):
时间 | 实体 | 业务 | 发生值 |
---|---|---|---|
2018-01-01 15:01:09 | X1 | A1 | 100 |
2018-01-01 10:06:04 | X1 | A2 | 100 |
2018-01-05 16:01:05 | X1 | A3 | 100 |
2018-01-03 17:10:09 | X2 | B3 | 100 |
2018-01-02 14:45:09 | X2 | B2 | 100 |
2018-01-01 15:08:45 | X3 | B1 | 100 |
那么我们能接触到的原始数据就是:
时间 | 实体 | 业务指标 | 业务指标 |
---|---|---|---|
2018-01 | X | A | 400 |
2018-01 | X | B | 200 |
这个数据粒度是比较粗的,这就意味着许多落实到每笔发生业务的算法无法被实现,这主要是由于传统行业的数据有较严的权限管控。
可以看到,数据的主要维度有:时间、实体、业务指标。可以看做是一个时间序列数据。
为什么说是基本的预处理呢,因为这部分的预处理内容不会对数据信息进行删减和改造,往往是扩展出不同维度出来。这些预处理后的数据也是传统非现场审计的业务人员主要用来执行审计工作的数据:
注意,缺失值的填补往往是具有业务意义的,所以最好和客户业务人员对接一下。缺失值填补有三种方法:
单指标算法
首先从简单的单指标算法开始讲起吧!单指标算法顾名思义就是只涉及到一个指标的算法,这类算法使用的数据只有两个维度:时间、实体。
标准分数(Standard Score,又称z-score,中文称为Z-分数或标准化值)在统计学中是一种无因次值,就是一种纯数字标记,是借由从单一(原始)分数中减去母体的平均值,再依照母体(母集合)的标准差分割成不同的差距,按照z值公式,各个样本在经过转换后,通常在正、负五到六之间不等。
我们通过和业务人员的交流发现,现在存在的大部分案例的表现都是极大极小的指标量(比如说某个月某业务发生实体的指标量远超其它实体,那么这个实体多半就是出现问题了),所以寻找极大极小值就成了一个目标。这里面异常检测的逻辑是:偏离样本平均水平的数据我们认为是异常。其实这是异常检测最基本的逻辑,基本上所有异常检测的算法都是基于这个假设的。
而寻找偏离均值的极大极小值也是一门不简单的学问。设置最大最小阈值是一个简单有效的方式,但是不智能。比如说我们观察了某指标之后,发现历史数据都是处在100以内的范围,那么我们可以设置阈值到了100,超过100的数据我们就认为是一个异常值;看起来很美好,但是实际上指标数据都是动态变化的,比如说到了新的一年,该指标出现了集体增长,我们又需要不断调整阈值吗?所以,一个自适应的智能算法才能解决这个问题。
如果没有数据分析经验的人可能看到定义就头疼,但是我保证你一定学过正态分布吧!Z分数实际上就是假设数据大致符合正态分布,那么理论上95.44%的数据都会集中在±2倍标准差区间内,99.7%的数据都会集中在±3倍标准差区间内。那么我们就可以定义在±2倍标准差区间以外的数据可能是极大极小值,是有审计监测的价值的,而在±3倍标准差区间以外的数据尤其值得注意。
而经过Z分数处理之后,数据会被scale为标准差为1、均值为0的数据,所以我们可以据此设计阈值,例如:
看看这个算法,经典、简单、有丰富的实践基础,有没有!
想象一份均值为1000的数据,突然塞入了一个1000000的数据,均值和标准差会被直接拉高特别多,那么在计算Z分数时就会出现发现除了那个超大值,其他值都是正常值(但实际上可能还是有些小幅偏离平均水平的异常值有审计风险)。
解决方案1:先用箱型图剔除超大值和超小值。箱型图其实也是一个简单高效的异常监测方法,可以作为Z分数之前的极端异常值排查步骤,找到那些严重拉偏数据集统计特征的数据点。
解决方案2:使用Modified Z-score算法替代Z-score算法。这个算法使用中位数替代均值来进行计算,有效的避免了超极端异常值对Z分数带来的影响。(参考资料:https://www.ibm.com/support/knowledgecenter/en/SSEP7J_11.1.0/com.ibm.swg.ba.cognos.ug_ca_dshb.doc/modified_z.html)
如果把所有数据一起进行Z分数的计算,你会发现被预警的实体总是一些大业务量的实体,因为这些实体的业务体量着实比较大,这是由各个实体本身的业务体量决定的。
可能你们会想到,将每个实体自己的数据放在一起做Z分数,那么不就可以避免这种情况了吗?确实如此,但是一个实体的数据量往往只有十几二十个,参考价值肯定不如和其它实体放在一起比较。那么我们就想,有没有什么方法可以对机构按体量进行分组呢?当然是有的,那么久引入了一个概念:
对标组
何谓对标组呢?就是给实体分组,找到和他们对标的其它实体。比如说,体量超大的可以作为一组,他们彼此就是对方的对标组。对标组的划分方法可以分为以下几种:
Z-score的计算公式是,其中是数据集中的一个数据值,是数据集的均值,是数据集的标准差,而则是该数据值对应的Z分数。我们在项目中使用的是Python Scikit Learn中preprocessing模块的scale函数,以下是我找到的一些实现方式:
K-means是一个经典的聚类算法在此就不赘述算法原理(延伸阅读:wikipedia: k-平均算法),我们在项目中使用的是Python Scikit Learn中cluster模块的KMeans类,以下是我找到的一些实现方式:
注意点1:我们都知道k-means的聚类结果受到初始点的影响,在保守一些的行业,频繁出现不同的聚类结果是经不住考验的,所以客户要求我们将聚类结果固定下来。由于固定初始点可能会导致聚类结果不准确,我们就使用多次聚类并投票决定使用哪种聚类结果,最多票的聚类结果。
注意点2:如何确定k也是一个技术活,我们将k按照机构层级控制在一定范围内,如一级机构k的取值范围可以是3-5,然后再使用轮廓系数进行评价,选出最优的k。(延伸阅读:Wikipedia: Silhouette (clustering) 、sklearn.metrics.silhouette_score)
奥卡姆剃刀原则告诉我们:"简单的就是最好的。",Z分数算法作为单变量异常检测中最常用的算法之一,即简单又高效,向不懂算法的业务人员来说也易于理解。实际上这个算法在我们项目中也是主力,业务人员认为这种算法适用范围广、效果清晰显著。
如果你正想找一个单变量异常检测的算法,你一定要考虑一下Z分数。但是注意也不要以为这个算法可以通杀所有数据集,Z分数算法的应用往往受限于数据集,所以如何灵活使用各种技巧让数据集适用于这个算法是难点。
与单指标算法不同,多指标算法的前提假设是一个或多个指标与指标有关系,我们找到这种关系之后,偏离该关系过多的点我们将之定义为outlier。这样的关系可以分为两种,一种是回归分析,另一种是簇分析。
应用多指标算法的基础是一个或多个指标与指标有关系,如何证明这个关系是第一步。
最直观且简单的相关关系,就是业务人员总结的关系,这种关系是人为判断的。举个例子,根据逻辑和常识来说,菠菜销售额和蔬菜销售额是有一定关系的,因为一个是另一个的子集,并且菠菜销售额占蔬菜销售额的比例一般在时间维度上的变化是不大的。
在统计学中,皮尔森相关系数用于度量两个变量X和Y之间的相关程度(线性相关),其值介于-1与1之间。
可以看出,皮尔森相关系数对于线性相关的表现能够很好的捕获,但是对于非线性关系的相关性则表现较差。如果你的假设是线性关系,那么我推荐使用皮尔森相关系数来做相关性分析,以证明你的假设。因为皮尔森相关系数不仅具有标准的度量和上下限,更有计算简单等优点。
在我们这个项目中,皮尔森相关系数做出的贡献是极大的,因为后期指标拆分之后量级有了指数的提升,皮尔森相关系数简单计算的特性使得我们可以在短时间内快速找到两两指标之间的线性相关性。而且相关系数简单的数学原理,也让我们和业务人员解释更加方便,相比之下,我们虽然引入了适用于更多情况的互信息值的指标,但是业务人员更喜欢用皮尔森相关系数来判断相关关系。从中我们也可以看出来,算法并不是越复杂或者适用范围越广就越值得推崇,作为IT咨询类的公司,我们在给出数字化解决方案的时候,往往要更多考虑客户的切身需求,而不是一味地追求够新、够好。
线性关系清晰且简单明了,可是数据不可能所有关系都是线性的,非线性的关系也是值得关注的,为了衡量指标与指标之间的非线性关系,我们引入了互信息(Mutual Information)这个概念。互信息来源于信息论,是描述变量与变量之间的关系强弱的,这个关系不仅包括线性关系,还包括其他的非线性关系。通俗的说,互信息反映了一个变量变化另一个随之变化的程度。(延伸阅读:Wikipedia: 互信息)
互信息值看起来是一个很棒的相关关系分析方式吧?其实也不然,互信息值的使用面临两个难题,第一就是计算过程复杂、第二就是结果是绝对量。相对复杂的原理和算法,给没有信息论背景知识的PO或业务人员理解带来了很大的不便。结果是绝对量也导致PO和业务人员不知道设置多少阈值来判断哪些是关系比较强哪些是关系比较弱的指标对。
为了解决绝对指标这个问题,我们使用了MIC系数,即最大信息系数Maximal information coefficient。与MI不同,MIC会对数据进行采样,再进行计算,并且结果值是相对量。虽然计算复杂,但是MIC的结果易于比较,且结果更不容易受偶然因素或者outliner影响,成为了除皮尔森相关系数外我们判断相关关系的重要手段。
两个变量之间的皮尔逊相关系数定义为两个变量之间的协方差和标准差的商:
我们在项目中使用的是pandas里面的corr函数和复杂的SQL查询语句计算,以下是我找到的一些实现方法:
一般地,两个连续随机变量 X 和 Y 的互信息可以定义为:
我们在项目中使用的是Scikit Learn里面的mutual_info_regression方法计算的,我没有找到其他一些计算方法,如果有可以在回复里补充~
MIC的计算相对来说复杂许多,一言两语没有办法讲清楚,感兴趣的朋友可以在这篇文章中稍微了解一下。MIC的实现也比较少,其中minepy是做的比较好的一个,其提供了C、C++和Python的接口。
注意:minepy中mic的计算是依赖于compute_score函数的,想要计算mic首先要执行之。具体使用方式可以参考文档中的例子。
假设两个指标已经证明了有线性相关性,那么使用线性模型来拟合当然合情合理。线性模型是简单的回归模型,在这里我们要使用它来完成对异常点的监测,这是一种基于回归的异常检测模型。这种方法的核心思想就是将数据拟合模型之后,找到偏离模型较多的点,这些点就是我们要找的不符合该线性关系的点。
对于回归模型来说,衡量模型的表现常用的方法就是使用残差,何为残差?残差在数理统计中是指实际观察值与估计值(拟合值)之间的差。“残差”蕴含了有关模型基本假设的重要信息。如果回归模型正确的话, 我们可以将残差看作误差的观测值。
有了残差这个工具并不会是一劳永逸了,因为残差是绝对量,受量纲的影响比较大,那么对于较大的点和较小的点的残差的公平对待是很难的,为了消除量纲的影响,我们要引入相对残差的概念,何为相对残差?相对残差是一个概念,代表了去掉量纲比较残差的一个指标,是残差的衍生指标。我们对于不同的模型可能需要使用不同的相对残差。在我们的实践中,我们将相对残差定义为: 或。其实它们都代表了残差和值的比值关系,反映这个残差对于这个点的预测值影响的重要程度。
因为统计或者实际发生异常业务的情况,会产生极大极小值,这些极大极小值不仅和其他点相离甚远,甚至会将模型拉偏,因为这些点的存在,可能建模出来的线性模型中,一些正常的点拥有了大一些的残差。如何解决这些极端值对建模过程带来的影响成为了团队需要解决的首要问题。
其实有了单指标算法的经验,我们第一反应就是先将极端异常值排除开来,然后再进行线性模型的建模。但是双指标甚至是多指标的极端异常值,并不能通过箱型图的方式排除,所以我们使用了多次建模的方式:第一次建模将拉偏模型的极端异常值用残差排除,然后第二次建模才是真正的寻找一些没那么夸张的异常点。
那如何判断第一次建模的时候哪些点是极端异常值呢?将所有点的相对残差做一次Z-Score,找到±3σ以外的点,这些点就是极端异常值。这样做避开了对于阈值判定的问题;如果模型没有极端异常值,也不会因为做了这个步骤标记了不正确的极端异常值。
所以整个建模过程如下所示:
线性模型的基本实现是基于最小二乘法网上一抓一大把,在这里就不赘述了。我们在项目中使用的scikit learn中的LinearRegression模型,我找了一些实现方法如下:
将线性模型换成曲线拟合回归模型,使用相同的建模方式,即可捕获非线性关系。我们在项目中,对于非线性的指标对,使用了随机森林回归模型替代线性模型。
对于强线性相关的指标来说,线性模型这种异常检测方法表现很好,并且线性模型可解释性较高,也更符合人的直觉。但是实际上,在业务上没有联系的指标对往往都不是简单的线性模型,即使使用了复杂的曲线拟合模型也能找到异常点,但在预警效果和可解释性上都表现的较差。非线性关系的模型往往容易把握不好过拟合和欠拟合之间的关系,特别我们没有确定的异常label,效果也很难衡量。有时候找到的点过多,有时候看起来和其他点偏离较多的点也抓不到。所以,我们认为线性关系之外的指标对,更适合用基于簇的异常检测算法。
提到异常检测就不可能不提到LOF这个算法,这个算法可以说是基于密度的异常检测最经典最具有普遍意义的算法。
我们先来理解一下这个算法可以做什么,如上图所示,对于C1集合的点,整体间距、密度、分散情况较为均匀一致,可以认为是同一簇;对于C2集合的点,同样可认为是一簇。o1、o2点相对孤立,可以认为是异常点或离散点。现在的问题是,如何实现算法的通用性,可以满足C1和C2这种密度分散情况迥异的集合的异常点识别。LOF可以实现我们的目标。
我本来打算把这个算法的实现详细的讲一下,因为这个算法太过经典了,但是我又不想让整篇文章过于冗长让读者们失去兴趣,所以如果有机会我会专门写一篇LOF原理和实现的博客。如果你现在就想了解更多,可以看看这篇文章:LOF离群因子检测算法及python3实现。
其实我们的目标和上面算法的目标一样,就是为了找到在簇与簇之间和之外的异常值。我们这里提到了簇,而在线性模型的时候却说数据呈线性关系,你可能要觉得疑惑了。其实这两个算法可以说是面向不同的目标的,有些指标对之间的关系并没有函数关系,二维簇的形式存在。在形成簇的情况下,线性模型或者其他函数拟合的模型的表现都会很好,而LOF算法就是为这种情况设计的,会有较好的效果。
LOF算法虽然是可以找到簇与簇之间和之外的异常点,但是这里你会发现簇之外的异常值其实和单指标算法的效果大部分是重叠的。所以这里LOF算法其实是为了找到那些隐藏在簇周围的异常点。
越是经典通用的算法,越是需要优秀的阈值设置办法,算法之所以"通用",就是因为其有参数这个调控因子。然而LOF得分是一个相对量,LOF 计算结果对于多大的值定义为异常值没有明确的规定。在一个平稳数据集中,可能 1.1 已经是一个异常值,而在另一个具有强烈数据波动的数据集中,即使 LOF 值为 2 可能仍是一个正常值。由于方法的局限性,数据集中的异常值界定可能存在差异所以我们面临的问题是如何选择一个好的k值和异常值阈值。
想象一下,我们的目标是异常值的LOF值尽可能的与普通值的LOF值拉开差距,那么就需要我们LOF得分的集合的方差足够大,这样才能尽可能把普通和异常的差距拉大。所以我们不妨用一系列的k进行测试,然后找到使结果LOF分数方差最大的k。在我们的应用中,我们用的是k=[3, 10]。
选择到了最佳的k,那么又如何来确定LOF得分的阈值呢?其实有了上面算法的例子,我们很容易就可以联想到使用统计方法来确定阈值:
因为这个算法很经典,所以我强烈建议有编程基础的朋友按照算法的原理手动实现一遍,我曾经在课程练习上实现过一次,虽然说并不难,但是要做到利用矩阵计算提升运算效率是需要好好琢磨一番。在项目中我们使用的是scikit learn中的sklearn.neighbors.LocalOutlierFactor。我试图找到其在spark等分布式框架的稳定实现,但是没有找到,我觉得有兴趣的朋友可以试着自己实现以下。
LOF的目标和效果可以说是一致的,我们的目标是找到那些簇周围的异常值,而其确确实实找得到。想象在某个月份,某个指标在大多数实体中都有突升突降的情况发生,这种突升突降往往在单指标模型和线性模型的情况下被报出预警,而实际上有可能是政策上的一个调整,LOF算法则不会预警,反而那些没有突升突降的实体可能会出现预警。所以LOF算法在这种情况的预警上是有很显著效果的。
可以预见的是,LOF算法不仅在双指标上有这样的表现,在单指标和在更高维数据上仍然有相当的效果,因为距离的度量在不同的维度下仍然有效。所以即使不是可视化的二维平面,我也强烈推荐你使用LOF来解决一些基于密度的异常值监测问题。
异常检测是一个很宽泛的主题,我没有办法在这篇文章里面覆盖到所有的角落,我们在实际做这个项目的时候其实还用了一些规则性算法、一些Ensemble类的算法,但是因为效果不稳定,和PO的预期也不相同,这里就不详细说了。其实和线性模型是一样的,核心思想都是将一个指标作为因变量而另一个作为自变量,用一个的函数去拟合。
异常检测同时也是个具体情况具体分析的应用场景,这篇文章并不是作为一篇介绍异常检测技术的文章,而是一篇可以在你无计可施的时候可以为你提供思路上的帮助的文章,希望看到这篇博客的人可以在以后遇到这种问题的时候有这样的经验。
需要更多有关于异常检测的帮助的话,Charu C. Aggarwal的《Outlier Analysis》是一本基本覆盖了主流所有异常检测算法的书,我们一致认为其可以作为异常检测项目的参考书。