0. 离群点挖掘方法
1. 背景
在LOF之前的异常检测算法大多是基于统计方法的,或是借用了一些聚类算法用于异常点的识别(如DBSCAN, OPTICS)。但这些方法都有一些不完美的地方:
1. 基于统计的方法:通常需要假设数据服从特定的概率分布,这个假设往往是不成立的;
2. 聚类方法:通常只能给出 0/1 的判断(是不是异常点),不能量化每个数据点的异常程度(如DBSCAN, OPTICS)。
相比较而言,基于密度的LOF(Local Outlier Factor,无监督)算法要更简单、直观。它不需要对数据的分布做太多要求,还能量化每个数据点的异常程度(outlierness)。
LOF最核心的部分是关于数据点密度的刻画。
2. 算法简介
基于密度的离群点检测方法有一个基本假设:非离群点对象周围的密度与其邻域周围的密度类似,而离群点对象周围的密度显著不同于其邻域周围的密度。LOF 就是基于密度来判断异常点的,通过给每个数据点都分配一个依赖于邻域密度的离群因子 LOF,进而判断该数据点是否为离群点以及离群程度。如果LOF>=1,则该点为离群点,如果LOF近似1,则该点为正常数据点。可以比较精确的量化每个数据点的异常程度。
LOF不会因为数据密度分散情况不同而错误的将正确点判定为异常点。
3. 几个基本概念
1. k-邻近距离(第k距离)
数据点P与其最近第k个样本点之间的距离称为点P的K-邻近距离,记为k-distance(p),公式如下:,其中点O为距离点P最近的第k个点,且在集合中至少有不包括P在内的k个点。
2. k-距离邻域
以点P为圆心,以k-邻近距离为半径的邻域(包括第k距离的点),称为P的第k距离邻域(不包括P),记作
。因此邻域内的样本数|
|>=k。
3. k-可达距离(reach-distance)
注意这里以点O为圆心,进行计算!
O到某个样本点P的第k-可达距离:,即”点O的k-邻近距离“和“点P与O之间的直接距离”的较大值,如图下所示。
离点O最近的k个点(O第k邻域内的点),O到他们的可达距离被认为是相等的,都等于点O的k-邻近距离。
小问题:为什么用局部可达距离,而不直接用距离?
用距离代替局部可达距离进行相应实验,效果相差不多。
为什么要定义一个可达距离呢?
这样会导致所有p接近o的统计波动d(p,o)可以显著减少,这种平滑效果的强度可以通过参数k来控制。k值越高,同一邻域内的对象的可达距离越相似,结果越平滑。
4. k-局部可达密度(local reachablity density)
数据点P的局部可达密度 = P的k个最近邻的平均可达距离的倒数(点P的第k邻域内点到p的平均可达距离的倒数)。距离越大,密度越小。
可以看出:如果P是一个离群点,那么它不太可能存在于中各点的k-距离邻域内,从而导致其局部可达密度lrd偏小;
注意:
点P的k-邻域点
到P的可达距离,不是点P到其邻域点的可达距离!(k-可达距离不是对称的),且如果有重复点(存在点p的k个重复值),那么分母的可达距离之和有可能为0,这样会导致lrd变为无限大。
局部可达密度lrd越高,越有可能属于同一簇;密度越低,越可能是离群点。即如果点P和周围邻域点是同一簇,那么可达距离越可能为较小的dk(O),导致可达距离之和较小,密度值较高;如果点P和周围邻居点较远,那么可达距离越可能取较大值d(O,P),导致密度较小,越可能是离群点;
5. k-局部异常因子LOF
根据局部可达密度的定义,如果一个数据点跟其他点比较疏远的话,那么显然它的局部可达密度就小。但LOF算法衡量一个数据点的异常程度,并不是看它的绝对局部密度,而是看它跟周围邻近的数据点的相对密度。这样做的好处是可以允许数据分布不均匀、密度不同的情况。局部异常因子即是用“局部相对密度”来定义的。数据点p的局部相对密度(局部异常因子)为点P邻居们的平均局部可达密度跟数据点P的局部可达密度的比值。
表示点P的邻域点的平均局部可达密度与点P的局部可达密度之比。LOF主要通过计算一个数值score来反映一个样本的异常程度。该数值表示:一个样本点周围样本点所处位置的平均密度比上该样本点所在位置的密度。
如果该比值接近1,说明P与其邻域点密度差不多,P可能和邻域属于同一簇;
如果该比值越小于1,说明P的密度高于其邻域点密度,P为密集点(内点);
如果该比值越大于1,说明P处的密度小于其邻域点密度,P越可能是异常点;
4. LOF算法流程
1. 对于每个数据点,计算它与其它所有点的距离,并按从近到远排序;
2. 对于每个数据点,找到它的k近邻节点,计算LOF得分;
1. 计算点P的局部可达密度;
2. 计算点P的k邻域内所有点局部可达密度的平均值;
3. 通过第二步计算结果与第一步计算结果的比值,得到LOF分数。
3. 如果LOF值越大,说明越异常,反之如果越小,说明越趋于正常。
具体案例参考:异常检测算法LOF - 知乎
圈圈圆圆圈圈,圈懵了多少人,异常检测算法LOF概念详解、可视化、实战案例分析 - 知乎
5. LOF优缺点
5.1 优点
1. 检测思想比较简单。算法简单易懂、容易实现,只需计算距离,且参数只有邻居个数K。
2. LOF同时考虑了数据集的局部和全局属性。异常值不是按绝对值确定的,而是相对于它们的邻域点密度确定的;
2. 当数据集中存在不同密度的不同集群时,LOF表现良好,比较适用于中等高维的数据集。
5.2 缺点
1. LOF算法中关于局部可达密度的定义其实暗含了一个假设:不存在 >=k 个重复的点。当这样的重复点存在时,这些点的平均可达距离为零,局部可达密度就变为无穷大,会给计算带来一些麻烦。在实际应用时,为了避免这样的情况出现,可以把 k-distance 改为 k-distinct-distance,不考虑重复的情况。或者还可以考虑给可达距离都加一个很小的值,避免可达距离等于零。
若有k个或以上的点跟O重合,即到O的距离是0,则ρ无法计算,要排除这种情况。或者,k-distance都加上一个很小的值,避免ρ无法计算。
2. 因需要计算距离,运算开销大,不适合高维、大数据。
LOF 需要计算数据点两两之间的距离,造成整个算法时间复杂度高,为O(n^2)。为了提高算法效率,后续有算法尝试改进。FastLOF算法先将整个数据随机的分成多个子集,然后在每个子集里计算LOF值。对于那些LOF异常得分<=1 的,从数据集里剔除,剩下的在下一轮寻找更合适的 nearest-neighbor,并更新 LOF 值。
6. API简介
局部离群点因子为每个样本的异常分数,主要通过每个点P和其邻域点的密度来判断该点是否为异常点,如果点P的密度越低,越可能被认为是异常点。而密度是通过点之间的距离计算的,距离越远,密度越低,距离越近,密度越高。因为LOF的密度是通过点的第k邻域来计算,而不是全局计算,因此得名“局部”异常因子。
6.1 构造函数
lof = LocalOutlierFactor(n_neighbors=20, # k值,默认=20,似乎实践很好,knn算法中的k默认为 5。
contamination='auto', # 样本中异常值占比,默认为0.1,取值0.1~0.5
algorithm='auto', # 如ball_tree、kd_tree、brute(暴力搜索)
leaf_size=30,
metric='minkowski', # 距离计算标准,距离标准图例
p=2, # 使用的距离度量函数(1:欧氏距离, 2:曼哈顿距离)
metric_params=None,
novelty=False, # 是否用于奇异值检测
n_jobs=None) # 并行作业数,-1时为CPU核心数
algorithm:找到最近的k个样本
1. kd_tree:依次对K维坐标轴,以中值切分,每一个节点是超矩形,适用于低维(<20时效率最高);
2. ball_tree:以质心c和半径r分割样本空间,每一个节点是超球体,适用于高维(kd_tree高维效率低);
3. brute:暴力搜索;
4. auto:通过 fit() 拟合,选择最适合的算法;如果数据稀疏,拟合后会自动选择brute,参数失效。
leaf_size:kd_tree和ball_tree中叶子大小,影响构建、查询、存储,根据实际数据而定。
树叶中可以有多于一个的数据点,算法在达到叶子时在其中执行暴力搜索即可。如果leaf size 趋向于N(训练数据的样本数量),算法其实就是 brute force了。如果leaf size 太小了,趋向于1,那查询的时候 遍历树的时间就会大大增加。
其他函数
decision_function(X)# 预测未知数据的异常程度,返回值为原始分数。分数越高,异常程度越高
fit(X[, y]) # 训练函数
fit_predict(X[, y]) # 训练&预测
get_params([deep]) # 获取模型参数
kneighbors([X, n_neighbors, return_distance]) # 查找点的K临近点
kneighbors_graph([X, n_neighbors, mode]) # 计算X中点的k临近点的(加权)图
predict([X]) # 结果预测
score_samples(X) # 与X的局部异常值因子相反
set_params(**params) # 设置此估算器的参数。
6.2 在一组数中找异常点
from sklearn.neighbors import LocalOutlierFactor
X = [[-1.1], [0.2], [100.1], [0.3]]
lof = LocalOutlierFactor(n_neighbors=2)
res = lof.fit_predict(X)
print(res)
# [ 1 1 -1 1]
print(lof.negative_outlier_factor_) # 返回-lof值
# [ -0.98214286 -1.03703704 -72.64219576 -0.98214286]
属性negative_outlier_factor_:和LOF相反的值,值越小,越有可能是异常值。
6.3 Outlier detection -- 异常值检测
当训练数据中包含离群点,通过相关算法找到训练数据的中心模式,忽视训练样本的其他异常点。(模型训练时要匹配训练数据的中心样本,忽略偏差观测值,从而检测出异常值)。
LOF既可以做奇异值检测,也可以做异常值检测。
neighbors.LocalOutlierFactor 算法用于奇异值检测时,需要将算法中novelty参数的值设置为True,即 lof = LocalOutlierFactor(novelty=True)。并且没有fit_predict(),此时需要用predict(), decision_function() 和 score_samples() 用于新的观测数据而不是训练数据。
X_inliers = 0.3 * np.random.randn(100, 2)
X_inliers = np.r_[X_inliers + 2, X_inliers - 2] # 正常点
X_outliers = np.random.uniform(low=-4, high=4, size=(20, 2)) # 异常点
X = np.r_[X_inliers, X_outliers] # 混合
ground_truth = np.ones(len(X), dtype=int)
ground_truth[-len(X_outliers):] = -1
lof = LocalOutlierFactor(n_neighbors=20, contamination=0.1)
res = lof.fit_predict(X)
y_pred = lof.fit_predict(X)
n_errors = (y_pred != ground_truth).sum()
X_scores = lof.negative_outlier_factor_
plt.title('Locla Outlier Factor (LOF)')
plt.scatter(X[:, 0], X[:, 1], color='k', s=3., label='Data points')
radius = (X_scores.max() - X_scores) / (X_scores.max() - X_scores.min())
plt.scatter(X[:, 0], X[:, 1], s=1000*radius, edgecolors='r',
facecolors='none', label='Outlier scores')
plt.axis('tight')
plt.xlim((-5, 5))
plt.ylim((-5, 5))
plt.xlabel("prediction errors: %d" %(n_errors))
legend = plt.legend(loc='upper left')
legend.legendHandles[0]._sizes = [10]
legend.legendHandles[1]._sizes = [20]
plt.show()
6.4 Novelty detection -- 奇异值检测
当训练数据中没有离群点,只含有positive(正常)的数据,我们的目标是用训练好的模型去检测另外发现的新样本。通过算法学习其pattern,之后用于检测未曾看到过新数据是否属于这个pattern,如果属于,该新数据是positive,否则negative,即奇异值。
# 生成网格坐标点
xx, yy = np.meshgrid(np.linspace(-5, 5, 500), np.linspace(-5, 5, 500))
X = 0.3 * np.random.randn(100, 2)
X_train = np.r_[X + 2, X - 2]
X = 0.3 * np.random.randn(20, 2)
X_test = np.r_[X + 2, X - 2]
# generate some abnormal novel observations
X_outliers = np.random.uniform(low=-4, high=4, size=(20, 2))
lof = LocalOutlierFactor(n_neighbors=20, contamination=0.1, novelty=True)
lof.fit(X_train)
y_pred_test = lof.predict(X_test)
y_pred_outliers = lof.predict(X_outliers)
n_error_test = y_pred_test[y_pred_test == -1].size
n_error_outliers = y_pred_outliers[y_pred_outliers == 1].size
Z = lof.decision_function(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
plt.title('Novelty Detection with LOF')
plt.contourf(xx, yy, Z, levels=np.linspace(Z.min(), 0, 7), cmap=plt.cm.PuBu)
a = plt.contour(xx, yy, Z, levels=[0], linewidths=2, colors='darkred')
plt.contourf(xx, yy, Z, levels=[0, Z.max()], colors='palevioletred')
s = 40
b1 = plt.scatter(X_train[:, 0], X_train[:, 1], c='white', s=s, edgecolors='k')
b2 = plt.scatter(X_test[:, 0], X_test[:, 1], c='blueviolet', s=s, edgecolors='k')
c = plt.scatter(X_outliers[:, 0], X_outliers[:, 1], c='gold', s=s, edgecolors='k')
plt.axis('tight')
plt.xlim((-5, 5))
plt.ylim((-5, 5))
plt.legend([a.collections[0], b1, b2, c],
["learned frontier", "training observations", "new regular observations", "new abnormal observations"],
loc='upper left',
prop=matplotlib.font_manager.FontProperties(size=11))
plt.xlabel("errors novel regular:%d/40; errors novel abnormal: %d/40" %(n_error_test, n_error_outliers))
plt.show()