在浏览器中进行深度学习:TensorFlow.js (十二)异常检测算法
异常检测是机器学习领域常见的应用场景,例如金融领域里的信用卡欺诈,企业安全领域里的非法入侵,IT运维里预测设备的维护时间点等。我们今天就来看看异常检测的基本概念,算法,然后看看如何利用TensorflowJS来进行异常检测。
什么是异常点?
异常点是指数据中和其它点不一样的点,异常检测就是要找到这些点。通常有以下这些不同类型的异常:
- 点异常 Point Anomalies
单个点和其它数据显著的不同
- 上下文异常 Contextual Anomalies
数据在所在的上下文环境中是个异常,例如下图t1不是异常而t2是因为t2前后的数据和t2有显著的差异。
- 集合异常 Collective Anomalies.
集合异常是指一组数据点和其它的数据有显著的不同,这一组数据的集合构成异常
从数据维度的角度来看,异常也分为单变量(univariate)和多变量异常(multivariate)。
异常检测的算法主要包括基于统计的算法和基于机器学习的算法。
异常检测的统计学方法
利用统计方法来进行异常检测有两种,第一种是参数化的,就是假定正常的数据是基于某种参数分布的,那么我们可以通过训练数据估计出数据的分布概率,那么对于每一个要分析的数据点都计算出该数据点在这个概率分布下生成的概率。这个值越高,说明该数据是正常点的可能性就越大,该数值越低,就说明这个点就越有可能是异常点。
最常见的方式就是ZScore,假定数据符合正态分布,ZScore计算数据点偏离均值多少个标准差。ZScore越大说明数据偏离均值越远,那么它是异常的概率就越高。
非参数化的方法并不假定数据的先验分布,数据的分布是从训练数据中学习而来的。
其它还有一些统计方法诸如:
- 时间序列中的移动平均值
- 卡曼滤波器
利用统计方法做异常检测非常容易理解,计算效率也很好。但是这种方法存在一些挑战:
- 数据点中的噪声和异常可能拥有类似的统计特征,那么就很难检测出来。
- 异常的定义可能会发生变化,一个固定的伐值可能并不适用。例如应用zscore,到底是大于3是异常还是大于4是异常,这很难定义。
异常检测的机器学习方法
从监督学习和非监督学习的角度来看,如果已经有了标记异常点的大量训练数据,异常检测可以简单的转化为分类问题,也就是数据分两类,正常点和异常点。但是在现实中,往往很难找到大量标记好异常点的训练数据,所以往往需要非监督学习来进行异常检测。
利用数据的相似度来检测异常的基本假设是,如果被检测的数据和已有的数据相似度大,那么它是正常数据的可能性就大。相似度的学习主要有基于距离的(KNN)和基于密度的(LOF)。
基于聚类的异常检测的基本假设是,正常数据聚集在一起,异常数据聚集在一起。
DBSCAN是异常检测常用的聚类方法。关于DBSCAN算法的介绍,大家可以参考我的博客图解机器学习
如上图所示,DBSCAN可以学习出正常聚类的中心点A,边缘点BC以及异常点N。
但是DBSCAN对于各个超参数的设定非常敏感,利用该方法虽然不需要标记异常点,但是找到合适的超参数并不容易。
支持向量机(SVM)是一种监督学习的分类方法,单类支持向量机(OneClassSVM)是SVM的一种扩展,可以用于非监督的检测异常。
该算法可以学习出正常点和异常点之间的边界。
隔离森林(isolation forests)是检测数据中异常值或新颖性的一种有效方法。这是一种基于二元决策树的方法。
隔离森林的基本原则是异常值很少,而且与其他观测结果相差甚远。为了构建树(训练),算法从特征空间中随机选取一个特征,并在最大值和最小值之间随机选择一个随机分割值。这是针对训练集中的所有观察结果。为了建造森林,树木整体被平均化为森林中的所有树木。
然后,为了预测,它将观察与“节点”中的分裂值进行比较,该节点将具有两个节点子节点,在该子节点上将进行另一次随机比较。由算法为实例做出的“分裂”的数量被命名为:“路径长度”。正如预期的那样,异常值的路径长度将比其他观察值更短。
利用深度学习进行异常检测
好了我们了解了异常检测的基本概念和方法,那么如何利用深度学习来进行异常检测呢?
虽然神经网络的主要应用是监督学习,但是其实也可以利用它来进行非监督学习,这里我们就需要了解自编码器(Autoencoder)了。
自编码器就是类似上图的一个网络,包含编码和解码两个主要的部分,我们利用训练数据集对该网络进行训练,输出的目标等于输入的数据。也就是说我们训练了一个可以重建输入数据的深度神经网络。那么这样做有什么用能。
我们可以看出编码的过程其实类似一个PCA的降维过程,就是经过编码,找到数据中的主要成分,利用该主要成份能够重建原始数据,就好像数据压缩和解压缩的过程,用更少的数据来取代原始数据。对于一般的自编码器的应用,训练好的自编码器不会全部用于构建网络,一般是使用编码的部分来进行数据的特征提取,降维,以达到更有效的计算。
利用自编码器,我们假定正常数据通过自编码器应该会还原,也就是输入和输出是一样的,而对于异常数据,还原出来的数据和原始数据存在差异。基本假设就是还原出来的数据和输入数据差异越小,那么它是正常数据的可能性就越大,反之它是异常数据的可能性就越大。
下面我们就来看一个利用自编码器用tensorflowJS来检测信用卡欺诈数据的例子。数据集来自Kaggle,考虑到TensorflowJS在浏览器中的性能问题,我对原始数据取样10000条记录来演示。
加载数据
该数据经过kaggle处理,包含Time交易时间,Amount交易数额,V1-V28是经过处理后的特征,Class表示交易的类别,1为欺诈交易。
async function loadData(path) { return await d3.csv(path); } const dataset = await loadData( "https://cdn.jsdelivr.net/gh/gangtao/datasets@master/csv/creditcard_sample_raw.csv" );
数据预处理
function standarize(val, min, max) { return (val - min) / (max - min); } function prepare(dataset) { const processedDataset = dataset.map(item => { const obj = {}; for (let i = 1; i < 29; i++) { const key = `V${i}`; obj[key] = parseFloat(item[key]); } obj["Class"] = item["Class"]; obj["Time"] = parseFloat(item["Time"]); obj["Amount"] = parseFloat(item["Amount"]); return obj; }); const timeMax = d3.max(processedDataset.map(i => i.Time)); const timeMin = d3.min(processedDataset.map(i => i.Time)); const amountMax = d3.max(processedDataset.map(i => i.Amount)); const amountMin = d3.min(processedDataset.map(i => i.Amount)); processedDataset.forEach(item => { item.stdTime = standarize(item.Time, timeMax, timeMin); item.stdAmount = standarize(item.Amount, amountMax, amountMin); }); return processedDataset; } const preparedDataset = prepare(dataset);
在数据预处理阶段我们对Time和Amount做标准化处理使它的值在(0-1)之间。
生成训练数据集
function makeTrainData(dataset) { console.log(dataset.length); const normalData = dataset.filter(item => item.Class == "0"); const anomalData = dataset.filter(item => item.Class == "1"); const sliceIndex = normalData.length*0.8; const normalTrainData = normalData.slice(0,sliceIndex); const normalTestData = normalData.slice(sliceIndex+1, normalData.length); console.log(normalData.length); const trainData = { x: [], y: [] }; normalTrainData.forEach(item => { const row = []; for (let i = 1; i < 29; i++) { const key = `V${i}`; row.push(item[key]); } row.push(item["stdAmount"]); row.push(item["stdTime"]); trainData.x.push(row); trainData.y.push(row); }); const testData = normalTestData.map(item => { const row = []; for (let i = 1; i < 29; i++) { const key = `V${i}`; row.push(item[key]); } row.push(item["stdAmount"]); row.push(item["stdTime"]); return row; }); const testAnomalData = anomalData.map(item => { const row = []; for (let i = 1; i < 29; i++) { const key = `V${i}`; row.push(item[key]); } row.push(item["stdAmount"]); row.push(item["stdTime"]); return row; }); return [trainData, testData, testAnomalData]; } const [trainData, testData, testAnomalData] = makeTrainData(preparedDataset);
我们选择80%的正常数据做训练,另外20%的正常交易数据和所有的异常交易数据做测试。
构建模型和训练
function buildModel() { const model = tf.sequential(); //encoder Layer const encoder = tf.layers.dense({ inputShape: [INPUT_NUM], units: FEATURE_NUM, activation: "tanh" }); model.add(encoder); const encoder_hidden = tf.layers.dense({ inputShape: [FEATURE_NUM], units: HIDDEN_NUM, activation: "relu" }); model.add(encoder_hidden); //decoder Layer const decoder_hidden = tf.layers.dense({ units: HIDDEN_NUM, activation: "tanh" }); model.add(decoder_hidden); //decoder Layer const decoder = tf.layers.dense({ units: INPUT_NUM, activation: "relu" }); model.add(decoder); //compile const adam = tf.train.adam(0.005); model.compile({ optimizer: adam, loss: tf.losses.meanSquaredError }); return model; } async function watchTraining() { const metrics = ["loss", "val_loss", "acc", "val_acc"]; const container = { name: "show.fitCallbacks", tab: "Training", styles: { height: "1000px" } }; const callbacks = tfvis.show.fitCallbacks(container, metrics); return train(model, data, callbacks); } async function trainBatch(data, model) { const metrics = ["loss", "val_loss", "acc", "val_acc"]; const container = { name: "show.fitCallbacks", tab: "Training", styles: { height: "1000px" } }; const callbacks = tfvis.show.fitCallbacks(container, metrics); console.log("training start!"); tfvis.visor(); // Save the model // const saveResults = await model.save('downloads://creditcard-model'); const epochs = config.epochs; const results = []; const xs = tf.tensor2d(data.x); const ys = tf.tensor2d(data.y); const history = await model.fit(xs, ys, { batchSize: config.batchSize, epochs: config.epochs, validationSplit: 0.2, callbacks: callbacks }); console.log("training complete!"); return history; } const model = buildModel(); model.summary(); const history = await trainBatch(trainData, model);
我们的自编码器的模型如下:
_________________________________________________________________ Layer (type) Output shape Param # ================================================================= dense_Dense1 (Dense) [null,16] 496 _________________________________________________________________ dense_Dense2 (Dense) [null,8] 136 _________________________________________________________________ dense_Dense3 (Dense) [null,8] 72 _________________________________________________________________ dense_Dense4 (Dense) [null,30] 270 ================================================================= Total params: 974 Trainable params: 974 Non-trainable params: 0
前两层是编码,后两层是解码。
分析异常值
自编码器模型训练好了以后我们就可以用它来分析异常,我们对测试数据的正常交易记录和异常交易记录用该模型预测,理论上正常交易的输出更接近原始值,而异常交易记录应该偏离原始值比较多,我们利用欧式距离来分析自编码器的输出结果。
async function distance(a, b ){ const axis = 1; const result = tf.pow(tf.sum(tf.pow(a.sub(b), 2), axis), 0.5); return result.data(); } async function predict(model, input) { const prediction = await model.predict(tf.tensor(input)); return prediction; } const predictNormal = await predict(model, testData); const predictAnomal = await predict(model, testAnomalData); const distanceNormal = await distance(tf.tensor(testData), predictNormal); const distanceAnomal = await distance(tf.tensor(testAnomalData), predictAnomal); const resultData = []; distanceNormal.forEach(item => { const obj = {}; obj.type = "normal"; obj.value = item; obj.index = Math.random(); resultData.push(obj); }) distanceAnomal.forEach(item => { const obj = {}; obj.type = "outlier"; obj.value = item; obj.index = Math.random(); resultData.push(obj); })
测试结果如下图:
上图绿色是异常交易,蓝色是正常交易。因为正常交易的数量较多,我们可能看不太清楚,我们分别显示如下图:
我们看到异常交易的自编码器输出和原始结果的距离都是大于10的,而绝大部分正常交易集中在10以下的区域,如果我们以10为伐值,应该可以找到大部分的异常交易,当然会有大量的正常交易误报。也就是该模型是无法做到完全的分辨正常和异常交易的。
完整的代码见我的Codepen
总结
本文介绍了各种异常检测的主要方法,无论是统计方法,机器学习的方法还是深度学习的方法,其中主要问题都是对于伐值或者参数的设置。
对于统计方法,需要确定究竟生成概率多少的事件是异常是百年一遇的洪水是异常,还是千年一遇的洪水是异常?
对于各种监督学习,我们往往缺乏异常点的标记,而对于非监督学习,调整各种参数会对异常点的判断有很大的影响。
对于基于自编码器的方法而言,我们看到,我们利用利用自编码器的输出和输入的差异来判断该事件是否为异常事件,然而究竟偏离多少来定义为异常,仍然需要用户来指定。
我们希望的完全通过数据和算法来自动发现异常仍然是一个比较困难的问题。
参考
- Introduction to Anomaly Detection
- A Brief Overview of Outlier Detection Techniques
- A Density-based algorithm for outlier detection
- Introduction to Anomaly Detection: Concepts and Techniques
- Anomaly detection using deep learning to measure quality of Large Datasets
- Comparing anomaly detection algorithms for outlier detection on toy datasets
- 莫烦 Python 自编码 (Autoencoder)
- Credit Card Fraud Detection Dataset
- 在浏览器中进行深度学习:TensorFlow.js (九)训练词向量 Word Embedding
- 在浏览器中进行深度学习:TensorFlow.js (七)递归神经网络 (RNN)
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Spring中如何使用责任链模式
关于责任链模式,其有两种形式,一种是通过外部调用的方式对链的各个节点调用进行控制,从而进行链的各个节点之间的切换;另一种是链的每个节点自由控制是否继续往下传递链的进度,这种比较典型的使用方式就是Netty中的责任链模式。本文主要讲解我们如何在Spring中使用这两种责任链模式。 1. 外部控制模式 对于外部控制的方式,这种方式比较简单,链的每个节点只需要专注于各自的逻辑即可,而当前节点调用完成之后是否继续调用下一个节点,这个则由外部控制逻辑进行。这里我们以一个过滤器的实现逻辑为例进行讲解,在平常工作中,我们经常需要根据一系列的条件对某个东西进行过滤,比如任务服务的设计,在执行某个任务时,其需要经过诸如时效性检验,风控拦截,任务完成次数等过滤条件的检验之后才能判断当前任务是否能够执行,只有在所有的过滤条件都完成之后,我们才能执行该任务。那么这里我们就可以抽象出一个Filter接口,其设计如下: public interface Filter { /** * 用于对各个任务节点进行过滤 */ boolean filter(Task task); } 这里的Filter.filter()方法...
- 下一篇
基于iview的router常用控制方式
1 iview的router控制需求 最近在使用iview框架写项目,遇到了一些路由控制上的问题,解决过程中也有一些心得,故在此记录下来. 每个项目在开发时,对于类似tags(标签页)的控制需求都不尽相同,故以下先列出本文所述项目对标签页的控制要求(如有不同需求,本文当也可提供一些思路): 对于同名(name)的路由标签页,不能打开多个.譬如说从商品列表中打开商品展示标签页,如果已经有在打开的商品编辑页面,则替换之.新打开的,未保存,已保存的标签页,同时只能存在一个(即不同params相同name的route只能有一个); 替换掉一个新的页面时,通过切换的方式切换回来(先切到其他标签页再切换回来),仍是原来页面的内容(即实际记录的params在替换后应变化).类似的情况,还应包含单据从未保存到已保存,以及保存并新增功能; 2 基于vue的router控制 iview是基于vue的框架,故vue本身自带的router控制方法是必然可行的. vue变更路由的常用方式参考以下(该方法在官方api中有更详细的介绍): //变更当前路由(有历史记录,建议使用此方式) this.$router.p...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2整合Redis,开启缓存,提高访问速度
- MySQL8.0.19开启GTID主从同步CentOS8
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS8编译安装MySQL8.0.19
- CentOS8安装Docker,最新的服务器搭配容器使用