首先,写这篇文章的初衷是为了记录自身对LOF的理解,另一个原因是个人在学习该算法的时候,也查阅过不少的文章或者视频,有一些知识点(如可达距离、局部可达密度等概念)可能并没有清晰的表达出来,因此该文章本着个人对该算法的理解记录学习该算法的过程,如有错误,请直接私信tinstone,希望对刚接触该算法的同学有所帮助,让知识传播下去。
Local Outlier Factor(LOF)是基于密度的经典算法,文章发表于 SIGMOD 2000。在LOF之前的异常检测算法大多是基于统计的方法(3α原则、箱型图等),或者是借用一些聚类算法的异常识别(如DBSCAN),而这些方法都有一些不完美的地方:
相比较而言,基于密度的LOF算法要更简单、直观。它不需要对数据的分布做太多的假设,还能量化的给出每个数据点的异常程度。
首先,基于密度的离群点检测方法有一个假设:非离群点对象周围的密度与其相邻点的周围的密度类似,而离群点对象周围的密度显著不同于其邻域周围点的密度。
通过下面的图片,可以直观的感受下:
聚集簇C1的点的分布间距比较大,即密度较小,但该簇内的点的密度都类似,而聚集簇C2 的点的分布间距更紧密,即密度更大,但该簇内的点的密度也都是类似的,再来观察O1、O2两个数据点,其周边的点数量较少或者密度较低,因此这两个点判定为异常点,因为基于假设得到:这两个点周围的密度显著不同于周围点的密度。
LOF,又称局部异常因子算法,从这个算法名称中我们先剧透一些信息:
有了上面的理解,这里一句话总结LOF的核心思想:通过计算一个score来反映一个样本的异常程度(该score是一个相对值,注意区别使用KNN进行异常检测的平均距离),如何理解score:一个样本点周围的样本的所处位置的平均密度比上该样本点所在位置的密度,比值越大于1,则该点所在位置的密度越小于其周围样本所在位置的密度, 这个点就越有可能是异常点。
在了解LOF之前,必须知道几个基本的概念,因为LOF是基于这几个概念来定义“局部”、“密度”、“异常因子”等相关概念的。
在距离数据点P最近的几个点中,第 k 个最近的点跟点 P 之间的距离称为点 P的 k邻近距离,记为k-distance(p),定义了局部异常因子算法中“局部”的概念,公式如下:
注:点 O 为距离点 P 最近的第 k 个点(k 为正整数,与KNN中的 k 的概念相同)。
比如上图中,距离点 P 最近的第 4 个点是 点4、5、6(即可能存在距离相等的点)。
这里的距离计算可以采用欧氏距离、汉明距离、马氏距离等等,通常采用欧氏距离(公式省略。。。)。
以点 P 为圆心,以 k邻近距离为半径画圆,这个圆以内的范围就是 k 距离邻域,公式如下:
还是以上图所示,假设 k=4,则 1-6均是邻域范围内的点。
点 P 到点 O 的第 k 可达距离(注意:该距离公式通常容易理解错误),公式如下:
这个可达距离需要特别注意:
通俗理解:点P到点O的可达距离实际上包含两层含义:
而可达距离代表的数学含义为:
点 P 的局部可达密度就是借助点 P到其 k邻域(“局部”)内的点(如点O,或者同心圆图例中的1-6的点)的可达距离的平均距离的倒数,具体公式如下:
取倒数的意义:使得距离与密度能达成认知上的一致性:距离越大,密度越小
根据局部可达密度的定义,如果一个数据点与周围数据点的平均密度相似(现在就只考虑密度,暂时忘记距离的概念,不然容易混),则其比值应该趋近于1,因此LOF算法衡量一个数据点的异常程度,并不是看它的绝对局部密度,而是看它跟周围邻近点的相对密度,即比值的概念,因此局部异常因子即是用局部密度来定义的,数据点 P 的局部异常因子为点 P 的 k邻域内的点的平均局部可达密度与点 P 的局部可达密度的比值,公式如下:
公式理解:
注:取相对密度的好处是可以允许数据分布不均匀、密度不同的情况存在,比如直观理解中C1、C2的点的分布
了解了LOF的相关概念之后,整个算法也就清晰明了了:
优点:
缺点:
Python实现LOF代码主要基于sklearn库,主要的代码参数如下:
from sklearn.neighbors import LocalOutlierFactor
主要参数:
n_neighbors:设置k, default=20
contamination: 设置样本中异常点的比例, default=0.1
主要属性:
negative_outlier_factor_: numpy array, shape(n_samples)
和LOF相反的值,值越小,越有可能是异常点,(注:上面提到的LOF的值越接近1, 越可能是正样本, LOF的值越大于1, 则越可能是异常样本)
主要方法:
fit_predict(x): 返回一个数组, -1,表示异常点, 1表示正常点
完整代码
# !/usr/bin/python
# -*- coding:utf-8 -*-
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from sklearn.neighbors import LocalOutlierFactor
print(__doc__)
np.random.seed(42)
xx, yy = np.meshgrid(np.linspace(-5, 5, 500), np.linspace(-5, 5, 500))
# Generate normal (not abnormal) training observations
X = 0.3 * np.random.randn(100, 2)
X_train = np.r_[X + 2, X - 2]
# Generate new normal (not abnormal) observations
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))
# fit the model for novelty detection (novelty=True)
clf = LocalOutlierFactor(n_neighbors=20, novelty=True, contamination=0.1)
clf.fit(X_train)
# DO NOT use predict, decision_function and score_samples on X_train as this
# would give wrong results but only on new unseen data (not used in X_train),
# e.g. X_test, X_outliers or the meshgrid
y_pred_test = clf.predict(X_test)
y_pred_outliers = clf.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
# plot the learned frontier, the points, and the nearest vectors to the plane
Z = clf.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()