内容概要: Task04:基于相似度的方法 - 图1

1. 概述

“异常”通常是一个主观的判断,什么样的数据被认为是“异常”的,需要结合业务背景和环境来具体分析确定。
实际上,数据通常嵌入在大量的噪声中,而我们所说的“异常值”通常指具有特定业务意义的那一类特殊的异常值。噪声可以视作特性较弱的异常值,没有被分析的价值。噪声和异常之间、正常数据和噪声之间的边界都是模糊的。异常值通常具有更高的离群程度分数值,同时也更具有可解释性。
在普通的数据处理中,我们常常需要保留正常数据,而对噪声和异常值的特性则基本忽略。但在异常检测中,我们弱化了“噪声”和“正常数据”之间的区别,专注于那些具有有价值特性的异常值。在基于相似度的方法中,主要思想是异常点的表示与正常点不同


2. 基于距离的度量

基于距离的方法是一种常见的适用于各种数据域的异常检测算法,它基于最近邻距离来定义异常值。 此类方法不仅适用于多维数值数据,在其他许多领域,例如分类数据,文本数据,时间序列数据和序列数据等方面也有广泛的应用。
基于距离的异常检测有这样一个前提假设,即异常点的 Task04:基于相似度的方法 - 图2 近邻距离要远大于正常点。解决问题的最简单方法是使用嵌套循环。 第一层循环遍历每个数据,第二层循环进行异常判断,需要计算当前点与其他点的距离,一旦已识别出多于 Task04:基于相似度的方法 - 图3 个数据点与当前点的距离在 Task04:基于相似度的方法 - 图4 之内,则将该点自动标记为非异常值。 这样计算的时间复杂度为Task04:基于相似度的方法 - 图5#card=math&code=O%28N%5E%7B2%7D%29),当数据量比较大时,这样计算是及不划算的。 因此,需要修剪方法以加快距离计算。


2.1 基于单元的方法

  在基于单元格的技术中,数据空间被划分为单元格,单元格的宽度是阈值D和数据维数的函数。具体地说,每个维度被划分成宽度最多为 Task04:基于相似度的方法 - 图6 单元格。在给定的单元以及相邻的单元中存在的数据点满足某些特性,这些特性可以让数据被更有效的处理。
image.png

以二维情况为例,此时网格间的距离为 Task04:基于相似度的方法 - 图8 ,需要记住的一点是,网格单元的数量基于数据空间的分区,并且与数据点的数量无关。这是决定该方法在低维数据上的效率的重要因素,在这种情况下,网格单元的数量可能不多。 另一方面,此方法不适用于更高维度的数据。对于给定的单元格,其 Task04:基于相似度的方法 - 图9 邻居被定义为通过最多1个单元间的边界可从该单元到达的单元格的集合。请注意,在一个角上接触的两个单元格也是 Task04:基于相似度的方法 - 图10 邻居。 Task04:基于相似度的方法 - 图11 邻居是通过跨越2个或3个边界而获得的那些单元格。 上图中显示了标记为 Task04:基于相似度的方法 - 图12的特定单元格及其 Task04:基于相似度的方法 - 图13Task04:基于相似度的方法 - 图14 邻居集。 显然,内部单元具有8个 Task04:基于相似度的方法 - 图15 邻居和40个 Task04:基于相似度的方法 - 图16 邻居。 然后,可以立即观察到以下性质:

  • 单元格中两点之间的距离最多为 Task04:基于相似度的方法 - 图17
  • 一个点与 Task04:基于相似度的方法 - 图18 邻接点之间的距离最大为 Task04:基于相似度的方法 - 图19
  • 一个点与它的 Task04:基于相似度的方法 - 图20 邻居(其中Task04:基于相似度的方法 - 图21 > 2)中的一个点之间的距离至少为Task04:基于相似度的方法 - 图22

image.png

唯一无法直接得出结论的是 Task04:基于相似度的方法 - 图24 中的单元格。 这表示特定单元中数据点的不确定性区域。 对于这些情况,需要明确执行距离计算。 同时,可以定义许多规则,以便立即将部分数据点确定为异常值或非异常值。 规则如下:

  • 如果一个单元格中包含超过 Task04:基于相似度的方法 - 图25 个数据点及其 Task04:基于相似度的方法 - 图26 邻居,那么这些数据点都不是异常值。
  • 如果单元 Task04:基于相似度的方法 - 图27 及其相邻 Task04:基于相似度的方法 - 图28Task04:基于相似度的方法 - 图29 中包含少于 Task04:基于相似度的方法 - 图30 个数据点,则单元A中的所有点都是异常值。

此过程的第一步是将部分数据点直接标记为非异常值(如果由于第一个规则而导致它们的单元格包含 Task04:基于相似度的方法 - 图31 个点以上)。 此外,此类单元格的所有相邻单元格仅包含非异常值。 为了充分利用第一条规则的修剪能力,确定每个单元格及其 Task04:基于相似度的方法 - 图32 邻居中点的总和。 如果总数大于 Task04:基于相似度的方法 - 图33 ,则所有这些点也都标记为非离群值。

接下来,利用第二条规则的修剪能力。 对于包含至少一个数据点的每个单元格 Task04:基于相似度的方法 - 图34,计算其中的点数及其 Task04:基于相似度的方法 - 图35Task04:基于相似度的方法 - 图36 邻居的总和。 如果该数字不超过 Task04:基于相似度的方法 - 图37,则将单元格Task04:基于相似度的方法 - 图38 中的所有点标记为离群值。 此时,许多单元可能被标记为异常值或非异常值。

对于此时仍未标记为异常值或非异常值的单元格中的数据点需要明确计算其 Task04:基于相似度的方法 - 图39 最近邻距离。即使对于这样的数据点,通过使用单元格结构也可以更快地计算出 Task04:基于相似度的方法 - 图40 个最近邻的距离。考虑到目前为止尚未被标记为异常值或非异常值的单元格Task04:基于相似度的方法 - 图41。这样的单元可能同时包含异常值和非异常值。单元格 Task04:基于相似度的方法 - 图42 中数据点的不确定性主要存在于该单元格的 Task04:基于相似度的方法 - 图43 邻居中的点集。无法通过规则知道 Task04:基于相似度的方法 - 图44Task04:基于相似度的方法 - 图45 邻居中的点是否在阈值距离 Task04:基于相似度的方法 - 图46 内,为了确定单元 Task04:基于相似度的方法 - 图47 中数据点与其Task04:基于相似度的方法 - 图48 邻居中的点集在阈值距离 Task04:基于相似度的方法 - 图49 内的点数,需要进行显式距离计算。对于那些在 Task04:基于相似度的方法 - 图50Task04:基于相似度的方法 - 图51 中不超过 Task04:基于相似度的方法 - 图52 个且距离小于 Task04:基于相似度的方法 - 图53 的数据点,则声明为异常值。需要注意,仅需要对单元 Task04:基于相似度的方法 - 图54 中的点到单元Task04:基于相似度的方法 - 图55Task04:基于相似度的方法 - 图56邻居中的点执行显式距离计算。这是因为已知 Task04:基于相似度的方法 - 图57 邻居中的所有点到 Task04:基于相似度的方法 - 图58 中任何点的距离都小于 Task04:基于相似度的方法 - 图59,并且已知 Task04:基于相似度的方法 - 图60Task04:基于相似度的方法 - 图61#card=math&code=%28r%3E%202%29) 的所有点与 Task04:基于相似度的方法 - 图62上任何点的距离至少为 Task04:基于相似度的方法 - 图63。因此,可以在距离计算中实现额外的节省。

2.2 基于索引的方法

对于一个给定数据集,基于索引的方法利用多维索引结构(如 Task04:基于相似度的方法 - 图64 树、Task04:基于相似度的方法 - 图65 树)来搜索每个数据对象 Task04:基于相似度的方法 - 图66 在半径 Task04:基于相似度的方法 - 图67 范围 内的相邻点。设 Task04:基于相似度的方法 - 图68 是一个异常值在其 Task04:基于相似度的方法 - 图69 -邻域内允许含有对象的最多个数,若发现某个数据对象 Task04:基于相似度的方法 - 图70Task04:基于相似度的方法 - 图71 -邻域内出现 Task04:基于相似度的方法 - 图72 甚至更多个相邻点, 则判定对象 Task04:基于相似度的方法 - 图73 不是异常值。该算法时间复杂度在最坏情况下为 Task04:基于相似度的方法 - 图74%2C#card=math&code=O%5Cleft%28k%20N%5E%7B2%7D%5Cright%29%2C) 其中 Task04:基于相似度的方法 - 图75 是数据集维数, Task04:基于相似度的方法 - 图76 是数据集包含对象的个数。该算法在数据集的维数增加时具有较好的扩展性,但是时间复杂度的估算仅考虑了搜索时间,而构造索引的任务本身就需要密集复杂的计算量。


3. 基于密度的度量

基于密度的算法主要有局部离群因子(LocalOutlierFactor,LOF),以及LOCI、CLOF等基于LOF的改进算法。下面我们以LOF为例来进行详细的介绍和实践。

基于距离的检测适用于各个集群的密度较为均匀的情况。在下图中,离群点B容易被检出,而若要检测出较为接近集群的离群点A,则可能会将一些集群边缘的点当作离群点丢弃。而LOF等基于密度的算法则可以较好地适应密度不同的集群情况。
image.png

那么,这个基于密度的度量值是怎么得来的呢?还是要从距离的计算开始。类似k近邻的思路,首先我们也需要来定义一个“k-距离”。


3.1 k-距离(k-distance(p)):

对于数据集D中的某一个对象o,与其距离最近的k个相邻点的最远距离表示为k-distance(p),定义为给定点p和数据集D中对象o之间的距离d(p,o),满足:

  • 在集合D中至少有k个点 o’,其中Task04:基于相似度的方法 - 图78,满足Task04:基于相似度的方法 - 图79%E2%89%A4d(p%2Co)#card=math&code=d%28p%2Co%27%29%E2%89%A4d%28p%2Co%29)
  • 在集合D中最多有k-1个点o’,其中Task04:基于相似度的方法 - 图80,满足Task04:基于相似度的方法 - 图81%3Cd(p%2Co)#card=math&code=d%28p%2Co%3B%29%3Cd%28p%2Co%29)

直观一些理解,就是以对象o为中心,对数据集D中的所有点到o的距离进行排序,距离对象o第k近的点p与o之间的距离就是k-距离。
image.png


3.2 k-邻域(k-distance neighborhood):

由k-距离,我们扩展到一个点的集合——到对象o的距离小于等于k-距离的所有点的集合,我们称之为k-邻域:Task04:基于相似度的方法 - 图83%7D(%20P%20)%20%3D%20%5C%7B%20q%20%E2%88%88%20D%20%5Cbackslash%5C%7B%20p%20%5C%7D%20%E2%88%A3%20d%20(%20p%20%2C%20q%20)%20%E2%89%A4%20k%20%E2%88%92%20d%20i%20s%20t%20a%20n%20c%20e%20(%20p%20)%5C%7D%0A#card=math&code=N_%7Bk%20%E2%88%92%20d%20i%20s%20t%20a%20n%20c%20e%20%28%20p%20%29%7D%28%20P%20%29%20%3D%20%5C%7B%20q%20%E2%88%88%20D%20%5Cbackslash%5C%7B%20p%20%5C%7D%20%E2%88%A3%20d%20%28%20p%20%2C%20q%20%29%20%E2%89%A4%20k%20%E2%88%92%20d%20i%20s%20t%20a%20n%20c%20e%20%28%20p%20%29%5C%7D%0A)。

在二维平面上展示出来的话,对象o的k-邻域实际上就是以对象o为圆心、k-距离为半径围成的圆形区域。就是说,k-邻域已经从“距离”这个概念延伸到“空间”了。


3.3 可达距离(reachability distance):

有了邻域的概念,我们可以按照到对象o的距离远近,将数据集D内的点按照到o的距离分为两类:

  • Task04:基于相似度的方法 - 图84在对象o的k-邻域内,则可达距离就是给定点p关于对象o的k-距离;
  • Task04:基于相似度的方法 - 图85在对象o的k-邻域外,则可达距离就是给定点p关于对象o的实际距离。

给定点p关于对象o的可达距离用数学公式可以表示为:Task04:基于相似度的方法 - 图86%20%3D%20m%20a%20x%20%5C%7Bk%E2%88%92distance(%20o%20)%20%2C%20d%20(%20p%20%2C%20o%20)%5C%7D#card=math&code=r%20e%20a%20c%20h%E2%88%92d%20i%20s%20t_%20k%20%28%20p%20%2C%20o%20%29%20%3D%20m%20a%20x%20%5C%7Bk%E2%88%92distance%28%20o%20%29%20%2C%20d%20%28%20p%20%2C%20o%20%29%5C%7D)
这样的分类处理可以简化后续的计算,同时让得到的数值区分度更高。


3.4 局部可达密度(local reachability density):

我们可以将“密度”直观地理解为点的聚集程度,就是说,点与点之间距离越短,则密度越大。在这里,我们使用数据集D中给定点p与对象o的k-邻域内所有点的可达距离平均值的倒数(注意,不是导数)来定义局部可达密度。

给定点p的局部可达密度计算公式为:
Task04:基于相似度的方法 - 图87

由公式可以看出,这里是对给定点p进行度量,计算其邻域内的所有对象o到给定点p的可达距离平均值。给定点p的局部可达密度越高,越可能与其邻域内的点 属于同一簇;密度越低,越可能是离群点。

看到这个公式,可能很多同学会想为什么使用“1/”的形式,而不直接将分子分母颠倒。写成这样,是考虑到存在p的k-邻域内有重复点而使Task04:基于相似度的方法 - 图88%7D%20reach-dist%7BN_k%7D(p%2Co)#card=math&code=%5Csum%5Climits%7Bo%E2%88%88Nk%28p%29%7D%20reach-dist%7BN_k%7D%28p%2Co%29)为0的情况。因为0不能为分母,所以写成其倒数的形式。


3.5 局部异常因子:

image.png
表示点p的邻域Task04:基于相似度的方法 - 图90#card=math&code=N_k%28p%29)内其他点的局部可达密度与点p的局部可达密度之比的平均数。如果这个比值越接近1,说明o的邻域点密度差不多,o可能和邻域同属一簇;如果这个比值小于1,说明o的密度高于其邻域点密度,o为密集点;如果这个比值大于1,说明o的密度小于其邻域点密度,o可能是异常点。

最终得出的LOF数值,就是我们所需要的离群点分数。在sklearn中有LocalOutlierFactor库,可以直接调用。下面来直观感受一下LOF的图像呈现效果。

LocalOutlierFactor库可以用于对单个数据集进行无监督的离群检测,也可以基于已有的正常数据集对新数据集进行新颖性检测。在这里我们进行单个数据集的无监督离群检测。

  1. import numpy as np
  2. import pandas as pd
  3. import matplotlib.pyplot as plt
  4. from sklearn.neighbors import LocalOutlierFactor
  5. plt.rcParams['font.sans-serif'] = ['SimHei']
  6. plt.rcParams['axes.unicode_minus']=False
  7. pd.set_option('display.max_columns', None)
  8. pd.set_option('display.max_rows', None)

首先构造一个含有集群和离群点的数据集。该数据集包含两个密度不同的正态分布集群和一些离群点。但是,这里我们手工对数据点的标注其实是不准确的,可能有一些随机点会散落在集群内部,而一些集群点由于正态分布的特性,会与其余点的距离相对远一些。在这里我们无法进行区分,所以按照生成方式统一将它们标记为“集群内部的点”或者“离群点”。

  1. np.random.seed(61)
  2. # 构造两个数据点集群
  3. X_inliers1 = 0.2 * np.random.randn(100, 2)
  4. X_inliers2 = 0.5 * np.random.randn(100, 2)
  5. X_inliers = np.r_[X_inliers1 + 2, X_inliers2 - 2]
  6. # 构造一些离群的点
  7. X_outliers = np.random.uniform(low=-4, high=4, size=(20, 2))
  8. # 拼成训练集
  9. X = np.r_[X_inliers, X_outliers]
  10. n_outliers = len(X_outliers)
  11. ground_truth = np.ones(len(X), dtype=int)
  12. # 打标签,群内点构造离群值为1,离群点构造离群值为-1
  13. ground_truth[-n_outliers:] = -1
  1. plt.title('构造数据集 (LOF)')
  2. plt.scatter(X[:-n_outliers, 0], X[:-n_outliers, 1], color='b', s=5, label='集群点')
  3. plt.scatter(X[-n_outliers:, 0], X[-n_outliers:, 1], color='orange', s=5, label='离群点')
  4. plt.axis('tight')
  5. plt.xlim((-5, 5))
  6. plt.ylim((-5, 5))
  7. legend = plt.legend(loc='upper left')
  8. legend.legendHandles[0]._sizes = [10]
  9. legend.legendHandles[1]._sizes = [20]
  10. plt.show()

image.png

然后使用LocalOutlierFactor库对构造数据集进行训练,得到训练的标签和训练分数(局部离群值)。为了便于图形化展示,这里对训练分数进行了一些转换。

  1. # 训练模型(找出每个数据的实际离群值)
  2. clf = LocalOutlierFactor(n_neighbors=20, contamination=0.1)
  3. # 对单个数据集进行无监督检测时,以1和-1分别表示非离群点与离群点
  4. y_pred = clf.fit_predict(X)
  5. # 找出构造离群值与实际离群值不同的点
  6. n_errors = y_pred != ground_truth
  7. X_pred = np.c_[X,n_errors]
  8. X_scores = clf.negative_outlier_factor_
  9. # 实际离群值有正有负,转化为正数并保留其差异性(不是直接取绝对值)
  10. X_scores_nor = (X_scores.max() - X_scores) / (X_scores.max() - X_scores.min())
  11. X_pred = np.c_[X_pred,X_scores_nor]
  12. X_pred = pd.DataFrame(X_pred,columns=['x','y','pred','scores'])
  13. X_pred_same = X_pred[X_pred['pred'] == False]
  14. X_pred_different = X_pred[X_pred['pred'] == True]
  15. # 直观地看一看数据
  16. X_pred
x y pred scores
0 1.913701 2.087875 0.0 0.000494
1 1.999748 2.212225 0.0 0.005255
2 2.040673 2.133115 0.0 0.001521
3 1.791277 1.743218 0.0 0.015652
4 1.991693 1.770405 0.0 0.010113
215 1.260683 1.964384 0.0 0.125260
216 0.092434 -2.721035 0.0 0.108697
217 -3.148785 3.659612 0.0 0.825292
218 -1.124219 -1.593130 1.0 0.025813
219 -0.628741 -1.696327 1.0 0.054695

将训练分数(离群程度)用圆直观地表示出来,并对构造标签与训练标签不一致的数据用不同颜色的圆进行标注。

  1. plt.title('局部离群因子检测 (LOF)')
  2. plt.scatter(X[:-n_outliers, 0], X[:-n_outliers, 1], color='b', s=5, label='集群点')
  3. plt.scatter(X[-n_outliers:, 0], X[-n_outliers:, 1], color='orange', s=5, label='离群点')
  4. # 以标准化之后的局部离群值为半径画圆,以圆的大小直观表示出每个数据点的离群程度
  5. plt.scatter(X_pred_same.values[:,0], X_pred_same.values[:, 1],
  6. s=1000 * X_pred_same.values[:, 3], edgecolors='c',
  7. facecolors='none', label='标签一致')
  8. plt.scatter(X_pred_different.values[:, 0], X_pred_different.values[:, 1],
  9. s=1000 * X_pred_different.values[:, 3], edgecolors='violet',
  10. facecolors='none', label='标签不同')
  11. plt.axis('tight')
  12. plt.xlim((-5, 5))
  13. plt.ylim((-5, 5))
  14. legend = plt.legend(loc='upper left')
  15. legend.legendHandles[0]._sizes = [10]
  16. legend.legendHandles[1]._sizes = [20]
  17. plt.show()

image.png

可以看出,模型成功区分出了大部分的离群点,一些因为随机原因散落在集群内部的“离群点”也被识别为集群内部的点,但是一些与集群略为分散的“集群点”则被识别为离群点。
同时可以看出,模型对于不同密度的集群有着较好的区分度,对于低密度集群与高密度集群使用了不同的密度阈值来区分是否离群点。
因此,我们从直观上可以得到一个印象,即基于LOF模型的离群点识别在某些情况下,可能比基于某种统计学分布规则的识别更加符合实际情况。


4. 练习

4.1 学习使用PyOD库生成toy example并调用LOF算法

  1. from pyod.models.lof import LOF
  2. from pyod.utils.data import generate_data
  3. from pyod.utils.example import visualize
  4. from pyod.utils.data import evaluate_print
  5. contamination = 0.1 # 异常值的比例
  6. n_train = 2000 # 训练集样本容量
  7. n_test = 1000 # 测试样本容量
  8. # 生成样本数据集
  9. X_train, y_train, X_test, y_test = generate_data(n_train=n_train, n_test=n_test,
  10. n_features=2, contamination=contamination,
  11. random_state=2021)
  12. # 模型训练
  13. model_name = 'LOF'
  14. model = LOF(n_neighbors=10)
  15. model.fit(X_train)
  16. y_train_pred = model.labels_ # 训练集训练后生成的标签,0为正常值,1为异常值
  17. y_train_scores = model.decision_scores_ # 训练数据的异常值得分
  18. params = model.get_params() # 模型参数的估计量
  19. # 对测试集进行预测
  20. y_test_pred = model.predict(X_test) # 生成测试集的预测标签
  21. y_test_scores = model.decision_function(X_test) # 使用合适的探测器预测X的原始异常分数。
  22. # 打印评估结果
  23. print("\nParams: ", params)
  24. print("\nOn Training Data:")
  25. evaluate_print(model_name, y_train, y_train_scores)
  26. print("\nOn Test Data:")
  27. evaluate_print(model_name, y_test, y_test_scores)

Params: {‘algorithm’: ‘auto’, ‘contamination’: 0.1, ‘leaf_size’: 30, ‘metric’: ‘minkowski’, ‘metric_params’: None, ‘n_jobs’: 1, ‘n_neighbors’: 10, ‘p’: 2} On Training Data: LOF ROC:0.4881, precision @ rank n:0.1

On Test Data:

LOF ROC:0.5221, precision @ rank n:0.15

  1. # 评估结果的可视化
  2. visualize(model_name, X_train, y_train, X_test, y_test, y_train_pred,
  3. y_test_pred, show_figure=True, save_figure=True)

image.png

参考资料:

  1. LOF: Identifying Density-Based Local Outliers
  2. https://scikit-learn.org/stable/auto_examples/neighbors/plot_lof_outlier_detection.html?highlight=lof
  3. https://pyod.readthedocs.io/en/latest/pyod.models.html#module-pyod.models.lof