本地离群因子(LOF)是另一种有效的无监督学习方法,用于离群点检测。自从它在2000年代初被发明以来(Breunig等人,2000年[1]),它已被广泛应用于不同类型的问题。它是一种基于密度的技术,使用最近邻搜索来识别异常点。在本章中,我将首先解释其动机和算法,然后引导您通过在PyOD中的实践。在上一章的k-NN算法中,我展示了任何基于密度的算法对离群值都很敏感,并且很容易过拟合。解决方案是训练多个模型,然后汇总得分。通过汇总多个模型,过拟合的机会大大减少,预测准确性将得到提高。PyOD模块提供了几种方法来汇总结果:平均值,最大最大值(MOM),最大平均值(AOM)和平均最大值(MOA)。在本章中,我将仅演示平均方法。
(A)离群值可以是全局的或局部的
很容易察觉到离群值,但很难用数学定义来定义。一个与其他数据“相距较远”的数据点是一个离群值。但从什么意义上说呢?有一次我坐在一个海滩的岩石上看日落。有一群海鸥站在我旁边的岩石上,我注意到有一只灰色的海鸥独自站在另一块岩石上。从我坐的地方看,那只灰色的海鸥是一个离群值。然而,海滩很长,沿着海滩还有许多其他单独的海鸥。如果从局部到海滩的远处看,那只灰色的海鸥看起来并不像一个离群值。
我希望上面的故事能说明一个观点:离群值可以是全局离群值或局部离群值。如果一个数据点远离其邻域中的数据主体,那么它被认为是一个离群值,但如果从整个数据宇宙来看,它可能不是一个离群值。图(A)说明了局部和全局离群值。有两个数据簇,由蓝色和绿色簇表示。点O1和O2是全局离群值,点a1和a2是局部离群值。只关注全局视图的算法可能只能捕捉到O1和O2。如果我们需要识别局部离群值a1和a2,我们需要一种可以关注局部邻域的算法。我们意识到局部邻域中数据点的密度是关键。一个远离密集簇的数据点很容易被识别为局部离群值。但如果它靠近一个密度较低的簇,将一个数据点分类为局部离群值就不那么容易了。LOF将局部邻域的不同密度考虑在内,以识别局部离群值。LOF是离群值所在位置附近邻域的密度与离群值附近数据簇的密度之比。例如,点a1的邻域密度与蓝色簇的密度不同。点a1被识别为局部离群值。尽管绿色簇中的密度不同,LOF可以发现点a2。
(B)LOF是如何工作的?
LOF计算数据点与其邻居之间的密度偏差。如果一个数据点的密度远低于其邻居,则被视为异常值。在图(A)中,点a1所在的区域比蓝色聚类中的其他点密度要低得多。在图(A)中,蓝色聚类看起来比绿色聚类更密集。当数据具有不同密度时,LOF特别有效。点a2是绿色聚类的局部异常值。LOF可以将a2识别为局部异常值。
局部离群因子(LOF)专门设计用于调整不同局部密度的变化。我将LOF的形成分为五个步骤,按顺序逐步解释LOF。在这五个步骤中,第四步是涉及局部密度的步骤。第五步是LOF,它将一个点所在位置的邻域密度与附近数据聚类的密度进行比较。
-
第一步:K个邻居
-
第二步:K距离
-
第三步:可达距离(RD)
-
第四步:局部可达密度(LRD)
-
第五步:K邻居的局部离群因子:LOF(k)
图(B):可达距离(作者提供的图像)
首先,K-最近邻是指到达K个最近邻居的圆形区域。在图(B)中,点O的第一个、第二个和第三个最近邻居分别是p1、p2和p3。虚线圆是当K=3时的K个最近邻居的区域。这个定义与KNN相同。
其次,K-距离(o)是从点O到K个最近邻居的距离。距离可以用欧几里得距离或曼哈顿距离来衡量。
第三,点O的可达距离是K-距离(o)和任意点P与O之间距离的最大值。在上面的图(B)中,当K=3时,K-距离是到点P3的距离。从P2到O的距离是d(P2, O),小于点P3到O的距离,即K-距离(o)。因此,P2的可达距离是K-距离(o),即从P3到O的距离。
步骤1和2只是帮助定义步骤3中的可达性距离。可达性距离公式是为了减少与点O接近的所有点P的*d(p,o)*的统计波动。暂时只需记住它是任意点到点O的距离。可达性距离越长,点O越有可能是异常值。
在步骤4中,LRD是点O从其邻居处的平均可达性距离的倒数。LRD的低值意味着最近的数据体离点O很远。点的LRD用于与其K个邻居的平均LRD进行比较。
最后,在第5步中,LOF是点O的K个邻居的平均LRD与其自身LRD之比,如下公式所示。第一项是K个邻居的平均LRD。LOF是点p的LRD与其K个最近邻居的LRD之比的平均值。第二项是点O的LRD。
如果点O不是异常值,则邻居的平均LRD大致等于点O的LRD。点和其邻居的密度大致相等。在这种情况下,LOF几乎等于1。另一方面,如果点O是异常值,则第一项邻居的平均LRD将高于第二项。LOF将大于1。
LOF中距离比的使用确保可以考虑不同的局部密度。簇中数据点的LOF值通常接近1,而不管该簇的密度如何。例如,在图(A)中,无论是蓝色簇还是绿色簇中的数据点的LOF值都接近1,即使两个簇的密度不同。
一般来说,如果LOF> 1,则被视为异常值。该数据点与邻居的距离比预期的要远。另一方面,如果数据点位于密集的数据区域中,则不是异常值。其LOF值将接近1。
© 建模过程
在本书中,我采用以下建模过程进行模型开发、评估和结果解释。
-
模型开发
-
阈值确定
-
正常组和异常组的描述统计
通过异常值分数,您可以选择一个阈值,将具有高异常值分数的异常观测值与正常观测值分开。如果有任何先前的知识表明异常值的百分比不应超过1%,则可以选择一个导致约1%异常值的阈值。
两组之间特征的描述统计(如均值和标准差)对于传达模型的合理性非常重要。如果预期异常组中某个特征的均值高于正常组的均值,而结果却相反,那么这将是违反直觉的。在这种情况下,您应该调查、修改或删除该特征,并重新进行建模。
(C.1) 第一步:构建LOF模型
和之前一样,我将使用PyOD的generate_data()
实用函数生成百分之十的异常值。为了使情况更有趣,数据生成过程(DGP)将创建六个变量。虽然这个模拟数据集有目标变量Y,但无监督模型只使用X变量。Y变量仅用于验证。我将异常值的百分比设置为5%,即“contamination=0.05”。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pyod.utils.data import generate_data
contamination = 0.05 # percentage of outliers
n_train = 500 # number of training points
n_test = 500 # number of testing points
n_features = 6 # number of features
X_train, X_test, y_train, y_test = generate_data(
n_train=n_train,
n_test=n_test,
n_features= n_features,
contamination=contamination,
random_state=123)
X_train_pd = pd.DataFrame(X_train)
X_train_pd.head()
六个变量的前五条记录如下所示:
让我在散点图中绘制前两个变量。散点图中的黄色点是10%的异常值。正常观测值是紫色点。
# Plot
plt.scatter(X_train_pd[0], X_train_pd[1], c=y_train, alpha=0.8)
plt.title('Scatter plot')
plt.xlabel('x0')
plt.ylabel('x1')
plt.show()
使用PyOD构建模型非常容易,这要归功于其统一的API。下面我们声明并拟合模型,然后使用函数decision_functions()
为训练和测试数据生成异常值得分。
参数contamination=0.05
声明了一个5%的污染率。污染率是异常值的百分比。在大多数情况下,我们不知道异常值的百分比,所以可以根据任何先前的知识来赋值。PyOD默认的污染率为10%。这个参数不影响异常值得分的计算。PyOD使用它来推导出异常值得分的阈值,并应用函数predict()
来分配标签(1或0)。我在下面的代码中编写了一个名为count_stat()
的简短函数来显示预测的“1”和“0”值的计数。语法.threshold_
显示了在指定污染率下的阈值。任何高于阈值的异常值得分都被视为异常值。
from pyod.models.lof import LOF
lof = LOF(contamination=0.05)
lof.fit(X_train)
# Training data
y_train_scores = lof.decision_function(X_train)
y_train_pred = lof.predict(X_train)
# Test data
y_test_scores = lof.decision_function(X_test)
y_test_pred = lof.predict(X_test) # outlier labels (0 or 1)
def count_stat(vector):
# Because it is '0' and '1', we can run a count statistic.
unique, counts = np.unique(vector, return_counts=True)
return dict(zip(unique, counts))
print("The training data:", count_stat(y_train_pred))
print("The training data:", count_stat(y_test_pred))
# Threshold for the defined comtanimation rate
print("The threshold for the defined comtanimation rate:" , lof.threshold_)
lof.get_params()
(C.2) 第二步 — 确定LOF模型的合理阈值
PyOD有一个内置函数threshold_
,它根据污染率contamination
计算训练数据的阈值。由于默认的污染率是0.10,训练数据的阈值为1.2311,如下所示。它表示任何具有异常分数大于1.2311的观测值都被视为异常值。
确定阈值的第二种方法是使用PCA异常值分数的直方图,这样我们可以根据业务情况选择阈值。图©展示了分数的直方图。我们可以选择更保守的方法,选择一个较高的阈值,这将导致异常组中的异常值更少,但希望更精细。
import matplotlib.pyplot as plt
plt.hist(y_train_scores, bins='auto') # arguments are passed to np.histogram
plt.title("Histogram with 'auto' bins")
plt.xlabel('LOF outlier score')
plt.show()
(C.3) 第三步 — 展示LOF模型中正常组和异常组的汇总统计信息
正如第1章所解释的那样,两组之间特征的描述性统计信息(如均值和标准差)对于证明模型的合理性非常重要。
我创建了一个名为descriptive_stat_threshold()
的简短函数,用于显示基于阈值的正常组和异常组的特征大小和描述性统计信息。下面我仅仅使用了5%的阈值。您可以测试一系列阈值,以找到一个合理的异常组大小。
threshold = lof.threshold_ # Or other value from the above histogram
def descriptive_stat_threshold(df,pred_score, threshold):
# Let's see how many '0's and '1's.
df = pd.DataFrame(df)
df['Anomaly_Score'] = pred_score
df['Group'] = np.where(df['Anomaly_Score']< threshold, 'Normal', 'Outlier')
# Now let's show the summary statistics:
cnt = df.groupby('Group')['Anomaly_Score'].count().reset_index().rename(columns={'Anomaly_Score':'Count'})
cnt['Count %'] = (cnt['Count'] / cnt['Count'].sum()) * 100 # The count and count %
stat = df.groupby('Group').mean().round(2).reset_index() # The avg.
stat = cnt.merge(stat, left_on='Group',right_on='Group') # Put the count and the avg. together
return (stat)
descriptive_stat_threshold(X_train,y_train_scores, threshold)
表格(C.3)
上表展示了正常组和异常组的特征。它显示了正常组和异常组的计数和计数百分比。 “异常分数”是平均异常分数。提醒您使用特征名称标记特征以进行有效的演示。该表告诉我们几个重要的结果:
-
**异常组的大小:**异常组约为5%。请记住,异常组的大小由阈值确定。如果您选择更高的阈值,大小将缩小。
-
**平均异常分数:**异常组的平均异常分数远高于正常组(1.77>1.07)。您不需要过多解释分数。
-
**每个组中的特征统计:**上面显示异常组中特征的平均值小于正常组的平均值。异常组中特征的平均值应该更高还是更低取决于业务应用。重要的是,所有平均值都应与领域知识一致。
因为我们在数据生成中有地面真实性y_test
,我们可以生成混淆矩阵来了解模型性能。该模型表现良好,识别出了所有25个异常值。
Actual_pred = pd.DataFrame({'Actual': y_test, 'Anomaly_Score': y_test_scores})
Actual_pred['Pred'] = np.where(Actual_pred['Anomaly_Score']< threshold,0,1)
pd.crosstab(Actual_pred['Actual'],Actual_pred['Pred'])
表格(C.4)
(D) 通过聚合多个模型实现模型稳定性
许多异常检测算法,特别是基于邻近性和分布的算法,对异常值非常敏感,并容易出现过拟合。我们如何产生一个稳定的模型结果?解决办法是使用一系列超参数训练模型,然后聚合得分。这样可以大大降低过拟合的可能性,并提高预测准确性。PyOD模块提供了四种方法来聚合结果。记得使用pip install combo
安装相应的函数。你只需要使用一种方法来产生聚合结果。
因为任何基于密度的模型都可能对训练数据过拟合,所以仅依赖一个模型不是一个好主意。我将为一系列最近邻居创建20个LOF模型。这些模型的平均预测将成为最终的模型预测。
下面的“y_by_average”是训练数据的20个模型预测的平均值,这些值存储在数据框“train_scores_norm”中。我在图(D)中创建了它的直方图。
from pyod.models.combination import aom, moa, average, maximization
from pyod.utils.utility import standardizer
from pyod.models.lof import LOF
# Standardize data
X_train_norm, X_test_norm = standardizer(X_train, X_test)
# Test a range of k-neighbors from 10 to 200. There will be 20 models.
n_clf = 20
k_list = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110,
120, 130, 140, 150, 160, 170, 180, 190, 200]
# Just prepare data frames so we can store the model results
train_scores = np.zeros([X_train.shape[0], n_clf])
test_scores = np.zeros([X_test.shape[0], n_clf])
train_scores.shape
# Modeling
for i in range(n_clf):
k = k_list[i]
lof = LOF(n_neighbors=k)
lof.fit(X_train_norm)
# Store the results in each column:
train_scores[:, i] = lof.decision_scores_
test_scores[:, i] = lof.decision_function(X_test_norm)
# Decision scores have to be normalized before combination
train_scores_norm, test_scores_norm = standardizer(train_scores,test_scores)
下面的“y_by_average”是训练数据的20个模型预测的平均值,这些值存储在数据框“train_scores_norm”中。我在图(D)中创建了它的直方图。
# Combination by average
# The test_scores_norm is 500 x 20. The "average" function will take the average of the 20 columns. The result "y_by_average" is a single column:
y_by_average = average(train_scores_norm)
import matplotlib.pyplot as plt
plt.hist(y_by_average, bins='auto') # arguments are passed to np.histogram
plt.title("Combination by average")
plt.show()
Figure (D): 训练数据的平均预测直方图
从**Figure (D)中,我可以将阈值设为0.5。然后我在Table (D)中生成了摘要统计表。它将25个数据点标识为异常值。读者应该对Table (C.3)**应用类似的解释。
descriptive_stat_threshold(X_train,y_train_by_average, .5)
表格(D)
(D) LOF摘要
-
LOF是一种基于密度的技术,使用最近邻搜索来识别异常点。
-
通常,如果LOF> 1,则被认为是异常值。该数据点距离邻居的距离比预期更远。另一方面,如果数据点位于密集的数据区域中,则不是异常值。其LOF值将接近1。
参考文献
-
[1] Breunig, M.M.; Kriegel, H.-P.; Ng, R.T.; Sander, J. LOF: Identifying density-based local outliers. In Proceedings of the 2000 ACM. SIGMOD international conference on Management of data — SIGMOD ’00, Dallas, TX, USA, 16–18 May 2000.
-
[2] He, Z., Xu, X., & Deng, S. (2003). Discovering cluster-based local outliers. Pattern Recognit. Lett., 24, 1641–1650.
异常检测系列文章导航
异常检测系列:Histogram-based Outlier Score_HBOS算法