一、基于距离的度量
“异常值”通常指具有特定业务意义的那一类特殊的异常值。噪声可以视作特性较弱的异常值,没有被分析的价值。噪声和异常之间、正常数据和噪声之间的边界都是模糊的。异常值通常具有更高的离群程度分数值,同时也更具有可解释性。
1.1 基于单元的方法
k近邻方法的一种延申版
基于距离的异常检测有这样一个前提假设,即异常点的 kkk 近邻距离要远大于正常点。解决问题的最简单方法是使用嵌套循环。 第一层循环遍历每个数据,第二层循环进行异常判断,需要计算当前点与其他点的距离,一旦已识别出多于 kkk 个数据点与当前点的距离在 DDD 之内,则将该点自动标记为非异常值。由于k近邻方法需要计算每个样本与其余样本之间的距离,这样计算复杂度就是O(N2)O(N^{2})O(N2)。 因此,需要修剪方法以加快距离计算。
一种基于单元格的方法可以减少计算量。我个人理解的话,基于单元格的方法主要原理是:给定一个阈值,把整个样本数据空间分成一些单元格的形式,单元格的大小与阈值和维度有关,分为单元格后就看这些单元格里面的数据点的个数与距离,也是一种看数据是密集的还是比较分散的。具体过程如下:
- 首先对于整个数据空间,把它分成一些单元格的形式,例如,一些二维数据,大小都在0-10,假设分为4个单元格,也就是(0-5)*(0-5)等,所以网格单元的数量基于数据空间的分区,与数据点的数量无关,就是同等空间下看数据密度的一种感觉。为了筛选出哪些是异常点,这里给定一个阈值D,把单元格的宽度设定在D2⋅d\frac{D}{{2 \cdot \sqrt d }}2⋅dD以内,d是数据维度。宽度的限制实际上,为了让一个单元格的两个点的距离最多为 D/2D/2D/2,也就是对角线的位置,对于单位超立方体,可以近似认为体对角线长度是d\sqrt dd。 另一方面,此方法不适用于更高维度的数据。这句话有点不是很理解,为什么更高维度不适用了。
- 把宽度限制后,我们再定义一些为了检测出异常值而需要的内容。对于给定的单元格,我们定义:
L1L_{1}L1 邻居: 通过最多1个单元间的边界可从该单元到达的单元格的集合,注意,在一个角上接触的两个单元格也是 L1L_{1}L1 邻居,我认为应该就是与原单元格相邻的单元格。
L2L_{2}L2邻居:通过跨越2个或3个边界而获得的那些单元格。
这个时候可以看下面这张图,更容易理解。
- 可以可以观察到以下性质:
i) 单元格中两点之间的距离最多为 D/2D/2D/2。
ii)单元格中任一个点与 L1L_{1}L1 邻接点之间的距离最大为 DDD。
iii) 一个点与它的 LrLrLr 邻居(其中rrr > 2)中的一个点之间的距离至少为DDD。
唯一无法直接得出结论的是 L2L_{2}L2 中的单元格。 这表示特定单元中数据点的不确定性区域。 对于这些情况,需要明确执行距离计算。 - 然后根据这些性质,以及k近邻方法的原理,我们给出如下判断法则:
i) 如果一个单元格及其 L1L_{1}L1 邻居中包含超过 kkk 个数据点,那么这些数据点都不是异常值。注意,该法则只用来筛选非异常值,其反命题不一定为真,并不是不超过k就是异常点。
ii) 如果单元格 AAA 及其相邻 L1L_{1}L1 和 L2L_{2}L2 中包含少于 kkk 个数据点,则单元A中的所有点都是异常值。 - 在进行完上面两个步骤后就可以选出大部分单元格里哪些点是异常点,哪些是非异常点,但是这两个规则并不能完全判别出所有的点。一旦两个规则都不符合的话就判断不出来。也就是如果单元格及其 L1L_{1}L1 邻居中数据点少于kkk个,而加上 L2L_{2}L2 的话又大于kkk个,那就判别不出来。但这些数据通过使用单元格结构也可以更快地计算出 kkk 个最近邻的距离。
- 对于尚未被标记为异常值或非异常值的单元格AAA。这样的单元可能同时包含异常值和非异常值。单元格 AAA 中数据点的不确定性主要存在于该单元格的 L2L_{2}L2 邻居中的点集。无法通过规则知道 AAA 的 L2L_{2}L2 邻居中的点是否在阈值距离 DDD 内,为了确定单元 AAA 中数据点与其L2L_{2}L2 邻居中的点集在阈值距离 DDD 内的点数,需要进行显式距离计算。对于那些在 L1L_{1}L1 和 L2L_{2}L2 中不超过 kkk 个且距离小于 DDD 的数据点,则声明为异常值。
注意:仅需要对单元 AAA 中的点到其L2L_{2}L2邻居中的点执行显式距离计算。
1.2 基于索引的方法
对于一个给定数据集,基于索引的方法利用多维索引结构(如 R\mathrm{R}R 树、k−dk-dk−d 树)来搜索每个数据对象 AAA 在半径 DDD 范围 内的相邻点。设 MMM 是一个异常值在其 DDD -邻域内允许含有对象的最多个数,若发现某个数据对象 AAA 的 DDD -邻域内出现 M+1M+1M+1 甚至更多个相邻点, 则判定对象 AAA 不是异常值.
2、基于密度的度量
基于密度实际上也是通过距离度量方法进行判定的。
基于密度的算法主要有局部离群因子(LocalOutlierFactor,LOF),以及LOCI、CLOF等基于LOF的改进算法。下面我们以LOF为例来进行详细的介绍和实践。
基于距离的检测适用于各个集群的密度较为均匀的情况。在下图中,离群点B容易被检出,而若要检测出较为接近集群的离群点A,则可能会将一些集群边缘的点当作离群点丢弃。而LOF等基于密度的算法则可以较好地适应密度不同的集群情况。
LOF主要也是看一个邻域内的数据密集程度。所以首先定义距离的方法,再定义邻域,然后定义密度的计算公式。
2.1 k-距离(k-distance§)
对于数据集D中的某一个对象p,与其距离最近的k个相邻点的最远距离表示为dk(O)d_k(O)dk(O) ,定义为给定点p和数据集D中对象o之间的距离d(p,o),满足:
- 在集合D中至少有k个点 o’,其中o′∈Dpo'∈D{p}o′∈Dp,满足d(p,o′)≤d(p,o)d(p,o')≤d(p,o)d(p,o′)≤d(p,o)
- 在集合D中最多有k-1个点o’,其中o′∈Dpo'∈D{p}o′∈Dp,满足d(p,o;)<d(p,o)d(p,o;)<d(p,o)d(p,o;)<d(p,o)
直观一些理解,就是以对象p为中心,点O是距离 P最近的第k 个点。
2.2 k-邻域(k-distance neighborhood)
由k-距离,我们扩展到一个点的集合——到对象O的距离小于等于k-距离的所有点的集合,我们称之为k-邻域Nk(O)N_k(O)Nk(O)。注意邻域只是具体的点的集合,而不是一个空间。而且k-邻域里点的个数一定是>=k>=k>=k的,因为可能有多个点与OOOO的距离等于k。
2.3 可达距离(reachability distance)
以OOO 为中心,点PPP到点 OOO 的第 kkk可达距离定义为:
- 若pip_ipi在对象o的k-邻域内,则可达距离就是给定点p关于对象o的k-距离;
- 若pip_ipi在对象o的k-邻域外,则可达距离就是给定点p关于对象o的实际距离。
即dk(P,O)=max(dk(O),d(P,O))d_k(P,O) = max ({ d_k(O),d(P,O) })dk(P,O)=max(dk(O),d(P,O))
2.4 局部可达密度
点PPP的局部可达密度计算公式为:
ρk(P)=1∑O∈Nk(P)dk(P,O)/∣Nk(P)∣=∣Nk(P)∣∑O∈Nk(P)dk(P,O)
\rho_{k}(P)=\frac{1}{\sum_{O \in N_{k}(P)} d_{k}(P, O) /\left|N_{k}(P)\right|}=\frac{\left|N_{k}(P)\right|}{\sum_{O \in N_{k}(P)} d_{k}(P, O)}
ρk(P)=∑O∈Nk(P)dk(P,O)/∣Nk(P)∣1=∑O∈Nk(P)dk(P,O)∣Nk(P)∣
- 此时对象是PPP
- 计算点PPP的第kkk邻域内的所有点到给点PPP的可达距离平均值
- PPP的局部可达密度越高,越可能与其邻域内的点 属于同一簇;密度越低,越可能是离群点。
2.5 局部离群因子
LOFk(P)=∑O∈Nk(P)ρk(O)ρk(P)∣Nk(P)∣
L O F_{k}(P)=\frac{\sum_{O \in N_{k}(P)} \frac{\rho_{k}(O)}{\rho_{k}(P)}}{\left|N_{k}(P)\right|}
LOFk(P)=∣Nk(P)∣∑O∈Nk(P)ρk(P)ρk(O)
注:表示点p的邻域Nk(p)N_k(p)Nk(p)内其他点的局部可达密度与点p的局部可达密度之比的平均数。如果这个比值越接近1,说明o的邻域点密度差不多,o可能和邻域同属一簇;如果这个比值小于1,说明o的密度高于其邻域点密度,o为密集点;如果这个比值大于1,说明o的密度小于其邻域点密度,o可能是异常点。
上图很好的展示了k=2时,点PPP的LOF计算原理。首先看点PPP的2-邻域有几个点,如图包含了O1O_1O1 、O2O_2O2。然后计算点PPP的局部可达密度,点O1O_1O1 、O2O_2O2的局部可达密度,这里注意点O1O_1O1 、O2O_2O2的局部可达密度是以它们自己为中心了,而不是PPP。
然后根据公式做平均。
三、作业
学习使用PyOD库生成toy example并调用LOF算法
from pyod.models.lof import LOF
import numpy as np
import pandas as pd
import pyod as po
import matplotlib.pyplot as plt
(X_train, y_train, X_test, y_test ) = po.utils.data.generate_data()
clf = LOF()
clf.fit(X_train)
y_pre = clf.labels_
X_normal = X_train[y_train == 0, :]
X_outlier = X_train[y_train == 1, :]
Xpre_normal = X_train[y_pre == 0, :]
Xpre_outlier = X_train[y_pre == 1, :]
fig = plt.figure(figsize=(20,20))
# 画出原始图像
plt.scatter(X_normal[:,0],X_normal[:,1], c = 'r',marker = '+' ,s = 50)
plt.scatter(X_outlier[:,0],X_outlier[:,1], c = 'b',marker = '+',s = 50 )
plt.scatter(Xpre_normal[:,0],Xpre_normal[:,1], c = 'r',marker = 'v', s=20 )
plt.scatter(Xpre_outlier[:,0],Xpre_outlier[:,1], c = 'b',marker = 'v', s=20)
plt.show()