异常检测学习笔记04 基于相似度的方法

异常检测——基于相似度的方法包括:
1.基于距离的度量
2.基于密度的度量
主要思想是异常点的表示与正常点不同

1. 基于距离的度量——适用各种数据域

所谓基于距离的度量,即通过最近邻距离来定义异常值。
其假设前提:异常点的 区分度太低不适合建索引_区分度太低不适合建索引 近邻距离要远大于正常点。
计算:嵌套循环。 第一层循环遍历每个数据,第二层循环进行异常判断,需要计算当前点与其他点的距离,一旦已识别出多于 区分度太低不适合建索引_区分度太低不适合建索引 个数据点与当前点的距离在 区分度太低不适合建索引_异常检测_03 之内,则将该点自动标记为非异常值。 这样计算的时间复杂度为区分度太低不适合建索引_数据_04,当数据量比较大时,需要修剪方法以加快距离计算。

1.1 基于单元的方法

数据空间被划分为单元格,单元格的宽度是阈值D和数据维数的函数。具体地说,每个维度被划分成宽度最多为 区分度太低不适合建索引_距离计算_05 单元格。在给定的单元以及相邻的单元中存在的数据点满足某些特性,这些特性可以让数据被更有效的处理。

区分度太低不适合建索引_区分度太低不适合建索引_06


以二维情况为例,此时网格间的距离为 区分度太低不适合建索引_距离计算_05 ,需要记住的一点是,网格单元的数量基于数据空间的分区,并且与数据点的数量无关。这是决定该方法在低维数据上的效率的重要因素,在这种情况下,网格单元的数量可能不多。 另一方面,此方法不适用于更高维度的数据。对于给定的单元格,其 区分度太低不适合建索引_距离计算_08 邻居被定义为通过最多1个单元间的边界可从该单元到达的单元格的集合。请注意,在一个角上接触的两个单元格也是 区分度太低不适合建索引_距离计算_08 邻居。 区分度太低不适合建索引_数据_10 邻居是通过跨越2个或3个边界而获得的那些单元格。 上图中显示了标记为 区分度太低不适合建索引_区分度太低不适合建索引_11的特定单元格及其 区分度太低不适合建索引_距离计算_08区分度太低不适合建索引_数据_10 邻居集。 显然,内部单元具有8个 区分度太低不适合建索引_距离计算_08 邻居和40个 区分度太低不适合建索引_数据_10

单元格中两点之间的距离最多为 区分度太低不适合建索引_异常检测_16
一个点与 区分度太低不适合建索引_距离计算_08 邻接点之间的距离最大为 区分度太低不适合建索引_异常检测_03
一个点与它的 区分度太低不适合建索引_距离计算_19 邻居(其中区分度太低不适合建索引_数据_20 > 2)中的一个点之间的距离至少为区分度太低不适合建索引_异常检测_03
  唯一无法直接得出结论的是 区分度太低不适合建索引_数据_10

如果一个单元格中包含超过 区分度太低不适合建索引_区分度太低不适合建索引 个数据点及其 区分度太低不适合建索引_距离计算_08 邻居,那么这些数据点都不是异常值。
如果单元 区分度太低不适合建索引_数据_25 及其相邻 区分度太低不适合建索引_距离计算_08区分度太低不适合建索引_数据_10 中包含少于 区分度太低不适合建索引_区分度太低不适合建索引 个数据点,则单元A中的所有点都是异常值。
  此过程的第一步是将部分数据点直接标记为非异常值(如果由于第一个规则而导致它们的单元格包含 区分度太低不适合建索引_区分度太低不适合建索引 个点以上)。 此外,此类单元格的所有相邻单元格仅包含非异常值。 为了充分利用第一条规则的修剪能力,确定每个单元格及其 区分度太低不适合建索引_距离计算_08 邻居中点的总和。 如果总数大于 区分度太低不适合建索引_区分度太低不适合建索引

接下来,利用第二条规则的修剪能力。 对于包含至少一个数据点的每个单元格 区分度太低不适合建索引_数据_25,计算其中的点数及其 区分度太低不适合建索引_距离计算_08区分度太低不适合建索引_数据_10 邻居的总和。 如果该数字不超过 区分度太低不适合建索引_区分度太低不适合建索引,则将单元格区分度太低不适合建索引_数据_25

对于此时仍未标记为异常值或非异常值的单元格中的数据点需要明确计算其 区分度太低不适合建索引_区分度太低不适合建索引 最近邻距离。即使对于这样的数据点,通过使用单元格结构也可以更快地计算出 区分度太低不适合建索引_区分度太低不适合建索引 个最近邻的距离。考虑到目前为止尚未被标记为异常值或非异常值的单元格区分度太低不适合建索引_数据_25。这样的单元可能同时包含异常值和非异常值。单元格 区分度太低不适合建索引_数据_25 中数据点的不确定性主要存在于该单元格的 区分度太低不适合建索引_数据_10 邻居中的点集。无法通过规则知道 区分度太低不适合建索引_数据_25区分度太低不适合建索引_数据_10 邻居中的点是否在阈值距离 区分度太低不适合建索引_异常检测_03 内,为了确定单元 区分度太低不适合建索引_数据_25 中数据点与其区分度太低不适合建索引_数据_10 邻居中的点集在阈值距离 区分度太低不适合建索引_异常检测_03 内的点数,需要进行显式距离计算。对于那些在 区分度太低不适合建索引_距离计算_08区分度太低不适合建索引_数据_10 中不超过 区分度太低不适合建索引_区分度太低不适合建索引 个且距离小于 区分度太低不适合建索引_异常检测_03 的数据点,则声明为异常值。需要注意,仅需要对单元 区分度太低不适合建索引_数据_25 中的点到单元区分度太低不适合建索引_数据_25区分度太低不适合建索引_数据_10邻居中的点执行显式距离计算。这是因为已知 区分度太低不适合建索引_距离计算_08 邻居中的所有点到 区分度太低不适合建索引_数据_25 中任何点的距离都小于 区分度太低不适合建索引_异常检测_03,并且已知 区分度太低不适合建索引_距离计算_19区分度太低不适合建索引_区分度太低不适合建索引_59 的所有点与 区分度太低不适合建索引_数据_25上任何点的距离至少为 区分度太低不适合建索引_异常检测_03。因此,可以在距离计算中实现额外的节省。

1.2 基于索引的方法

对于一个给定数据集,基于索引的方法利用多维索引结构(如 区分度太低不适合建索引_区分度太低不适合建索引_62 树、区分度太低不适合建索引_距离计算_63 树)来搜索每个数据对象 区分度太低不适合建索引_数据_25 在半径 区分度太低不适合建索引_异常检测_03 范围 内的相邻点。设 区分度太低不适合建索引_区分度太低不适合建索引_66 是一个异常值在其 区分度太低不适合建索引_异常检测_03 -邻域内允许含有对象的最多个数,若发现某个数据对象 区分度太低不适合建索引_数据_25区分度太低不适合建索引_异常检测_03 -邻域内出现 区分度太低不适合建索引_异常检测_70 甚至更多个相邻点, 则判定对象 区分度太低不适合建索引_数据_25 不是异常值。该算法时间复杂度在最坏情况下为 区分度太低不适合建索引_异常检测_72 其中 区分度太低不适合建索引_区分度太低不适合建索引 是数据集维数, 区分度太低不适合建索引_数据_74

2.基于密度的度量

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

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

区分度太低不适合建索引_区分度太低不适合建索引_75


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

2.1 k-距离(k-distance)

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

在集合D中至少有k个点 o’,其中区分度太低不适合建索引_距离计算_76,满足区分度太低不适合建索引_距离计算_77

在集合D中最多有k-1个点o’,其中区分度太低不适合建索引_距离计算_76,满足区分度太低不适合建索引_数据_79

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

区分度太低不适合建索引_距离计算_80

2.2 k-邻域(k-distance neighborhood)

由k-距离,我们扩展到一个点的集合——到对象o的距离小于等于k-距离的所有点的集合,我们称之为k-邻域。
在二维平面上展示出来的话,对象o的k-邻域实际上就是以对象o为圆心、k-距离为半径围成的圆形区域。就是说,k-邻域已经从“距离”这个概念延伸到“空间”了。

2.3 可达距离(reachability distance)

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

区分度太低不适合建索引_异常检测_81在对象o的k-邻域内,则可达距离就是给定点p关于对象o的k-距离;
区分度太低不适合建索引_异常检测_81在对象o的k-邻域外,则可达距离就是给定点p关于对象o的实际距离。
给定点p关于对象o的可达距离用数学公式可以表示为:区分度太低不适合建索引_数据_83
这样的分类处理可以简化后续的计算,同时让得到的数值区分度更高。

2.4 局部可达密度(local reachability density)

我们可以将“密度”直观地理解为点的聚集程度,就是说,点与点之间距离越短,则密度越大。在这里,我们使用数据集D中给定点p与对象o的k-邻域内所有点的可达距离平均值的倒数(注意,不是导数)来定义局部可达密度。
给定点p的局部可达密度计算公式为:区分度太低不适合建索引_区分度太低不适合建索引_84
由公式可以看出,这里是对给定点p进行度量,计算其邻域内的所有对象o到给定点p的可达距离平均值。给定点p的局部可达密度越高,越可能与其邻域内的点 属于同一簇;密度越低,越可能是离群点。

2.5 局部异常因子

区分度太低不适合建索引_数据_85


表示点p的邻域区分度太低不适合建索引_数据_86内其他点的局部可达密度与点p的局部可达密度之比的平均数。如果这个比值越接近1,说明o的邻域点密度差不多,o可能和邻域同属一簇;如果这个比值小于1,说明o的密度高于其邻域点密度,o为密集点;如果这个比值大于1,说明o的密度小于其邻域点密度,o可能是异常点。

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

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

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.neighbors import LocalOutlierFactor
np.random.seed(61)

# 构造两个数据点集群
X_inliers1 = 0.2 * np.random.randn(100, 2)
X_inliers2 = 0.5 * np.random.randn(100, 2)
X_inliers = np.r_[X_inliers1 + 2, X_inliers2 - 2]

# 构造一些离群的点
X_outliers = np.random.uniform(low=-4, high=4, size=(20, 2))

# 拼成训练集
X = np.r_[X_inliers, X_outliers]

n_outliers = len(X_outliers)
ground_truth = np.ones(len(X), dtype=int)
# 打标签,群内点构造离群值为1,离群点构造离群值为-1
ground_truth[-n_outliers:] = -1
plt.title('LOF')
plt.scatter(X[:-n_outliers, 0], X[:-n_outliers, 1], color='b', s=5, label='inliers')
plt.scatter(X[-n_outliers:, 0], X[-n_outliers:, 1], color='orange', s=5, label='outliers')

plt.axis('tight')
plt.xlim((-5, 5))
plt.ylim((-5, 5))
legend = plt.legend(loc='upper left')
legend.legendHandles[0]._sizes = [10]
legend.legendHandles[1]._sizes = [20]
plt.show()

区分度太低不适合建索引_距离计算_87

# 训练模型(找出每个数据的实际离群值)
clf = LocalOutlierFactor(n_neighbors=20, contamination=0.1)

# 对单个数据集进行无监督检测时,以1和-1分别表示非离群点与离群点
y_pred = clf.fit_predict(X)

# 找出构造离群值与实际离群值不同的点
n_errors = y_pred != ground_truth
X_pred = np.c_[X,n_errors]

X_scores = clf.negative_outlier_factor_
# 实际离群值有正有负,转化为正数并保留其差异性(不是直接取绝对值)
X_scores_nor = (X_scores.max() - X_scores) / (X_scores.max() - X_scores.min())
X_pred = np.c_[X_pred,X_scores_nor]
X_pred = pd.DataFrame(X_pred,columns=['x','y','pred','scores'])

X_pred_same = X_pred[X_pred['pred'] == False]
X_pred_different = X_pred[X_pred['pred'] == True]

# 直观地看一看数据
X_pred

区分度太低不适合建索引_区分度太低不适合建索引_88

plt.title('local Outlier (LOF)')
plt.scatter(X[:-n_outliers, 0], X[:-n_outliers, 1], color='b', s=5, label='inliers')
plt.scatter(X[-n_outliers:, 0], X[-n_outliers:, 1], color='orange', s=5, label='outlier')

# 以标准化之后的局部离群值为半径画圆,以圆的大小直观表示出每个数据点的离群程度
plt.scatter(X_pred_same.values[:,0], X_pred_same.values[:, 1], 
            s=1000 * X_pred_same.values[:, 3], edgecolors='c', 
            facecolors='none', label='incommon')
plt.scatter(X_pred_different.values[:, 0], X_pred_different.values[:, 1], 
            s=1000 * X_pred_different.values[:, 3], edgecolors='violet', 
            facecolors='none', label='uncommon')

plt.axis('tight')
plt.xlim((-5, 5))
plt.ylim((-5, 5))

legend = plt.legend(loc='upper left')
legend.legendHandles[0]._sizes = [10]
legend.legendHandles[1]._sizes = [20]
plt.show()

区分度太低不适合建索引_区分度太低不适合建索引_89


结论:

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

3.练习

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

from pyod.utils.data import evaluate_print,generate_data

# 1)生成toy example
contamination = 0.1  # percentage of outliers
n_train = 200  # number of training points

X_train, y_train = generate_data(n_train=n_train, contamination=contamination)

#2)调用LOF算法
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.neighbors import LocalOutlierFactor

n_outliers = 20
ground_truth = np.ones(len(X_train), dtype=int)
# 打标签,群内点构造离群值为1,离群点构造离群值为-1
ground_truth[-n_outliers:] = -1
# 训练模型(找出每个数据的实际离群值)
clf = LocalOutlierFactor(n_neighbors=20, contamination=0.1)

# 对单个数据集进行无监督检测时,以1和-1分别表示非离群点与离群点
y_pred = clf.fit_predict(X_train)

# 找出构造离群值与实际离群值不同的点
n_errors = y_pred != ground_truth
X_pred = np.c_[X_train,n_errors]

X_scores = clf.negative_outlier_factor_
# 实际离群值有正有负,转化为正数并保留其差异性(不是直接取绝对值)
X_scores_nor = (X_scores.max() - X_scores) / (X_scores.max() - X_scores.min())
X_pred = np.c_[X_pred,X_scores_nor]
X_pred = pd.DataFrame(X_pred,columns=['x','y','pred','scores'])

X_pred_same = X_pred[X_pred['pred'] == False]
X_pred_different = X_pred[X_pred['pred'] == True]

plt.title('local Outlier (LOF)')
plt.scatter(X_train[:-n_outliers, 0], X_train[:-n_outliers, 1], color='b', s=5, label='inliers')
plt.scatter(X_train[-n_outliers:, 0], X_train[-n_outliers:, 1], color='orange', s=5, label='outlier')

# 以标准化之后的局部离群值为半径画圆,以圆的大小直观表示出每个数据点的离群程度
plt.scatter(X_pred_same.values[:,0], X_pred_same.values[:, 1], 
            s=1000 * X_pred_same.values[:, 3], edgecolors='c', 
            facecolors='none', label='incommon')
plt.scatter(X_pred_different.values[:, 0], X_pred_different.values[:, 1], 
            s=1000 * X_pred_different.values[:, 3], edgecolors='violet', 
            facecolors='none', label='uncommon')

plt.axis('tight')
plt.xlim((-5, 5))
plt.ylim((-5, 5))

legend = plt.legend(loc='upper left')
legend.legendHandles[0]._sizes = [10]
legend.legendHandles[1]._sizes = [20]
plt.show()

区分度太低不适合建索引_距离计算_90