基于经验累积分布的异常检测(ECOD)具有非常直观的方法:异常值是分布尾部的罕见事件,可以通过测量分布中的位置来识别。
ECOD首先以非参数方式估计变量的分布。然后,它将所有维度的估计尾部概率相乘,以获得观测值的异常得分。从数学上讲,很难估计多个维度的联合分布。ECOD假设变量独立,因此可以估计每个变量的经验累积分布。尽管变量独立的假设可能过于严格,但这并不是新的,因为前一章中的HBOS也做出了相同的假设,并且已被证明是有效的。
(A) ECOD的优势是什么?
[1]的作者证明了ECOD优于其他流行的基准检测方法。由于ECOD没有需要调整的超参数,因此在处理大量数据时速度很快。作者报告称,在标准个人笔记本上,处理一个包含一百万观测和一万个特征的大型数据集只需要大约两个小时。
ECOD的另一个优点是易于解释。它允许您检查每个尾部概率如何对最终的异常得分做出贡献。我将在后面的部分演示其可解释性。
(B) ECOD是如何工作的?
很多读者熟悉参数分布,但不熟悉非参数分布。我将描述参数分布和非参数分布,并讨论非参数分布的形成。然后我将介绍ECOD算法,然后比较ECOD和HBOS。
(B.1) 理解经验累积分布函数
为了解释“非参数”和“参数”这些术语,甚至有助于澄清一些相关术语“总体”、“样本”和“估计”。统计学的目标是了解我们感兴趣的“总体”。诸如均值、标准差和比例之类的数量被称为描述总体的“参数”。我们通常无法获取整个总体的所有数据,因此无法计算描述总体的参数。一个实际的解决方案是收集随机的“样本”来描述总体。样本的分布使我们能够对总体的分布进行“估计”。
“参数化”方法对底层总体的分布形状(如正态分布)做出假设。“非参数化”方法不对总体分布的形状和参数做任何假设。分布将从样本中“经验性”地估计。
让我演示非参数化方法并经验性地估计一个分布。为了生成不遵循任何特定形状的分布,我将三个伽玛分布和一个正态分布任意聚合,如图(B.1)所示。右尾中可以找到一些极端值。
# Create a distribution that is the combination of three other distributions
from matplotlib import pyplot
from numpy.random import normal, gamma
from numpy import hstack
shape, scale = 10, 2.
s1 = gamma(shape, scale, 1000)
s2 = gamma(shape * 2, scale * 2, 1000)
s3 = normal(loc=0, scale=5, size=1000)
sample = hstack((s1, s2, s3))
# plot the histogram
pyplot.hist(sample, bins=50)
pyplot.show()
图(B.1): 一个分布
为了通过经验估计分布,我使用Python的statmodels
模块中的ECDF()
函数来推导累积分布函数(CDF),如图(B.2)所示。
# fit a cdf
from statsmodels.distributions.empirical_distribution import ECDF
sample_ecdf = ECDF(sample)
# plot the cdf
pyplot.plot(sample_ecdf.x, sample_ecdf.y)
pyplot.show()
图(B.2):经验累积分布函数(ECDF)
我选择了图(B.2)中的一些位置来展示累积概率达到这些位置的情况。例如,X<0的累积概率为0.173,X<125的累积概率为0.9967。或者我们可以说’0’位于17.3百分位,'125’位于99.67百分位。请注意,CDF接近1.0的位置意味着该点接近极值。这个特性将帮助我们找到极值。
print('P(x<-20): %.4f' % sample_ecdf(-20))
print('P(x<-2): %.4f' % sample_ecdf(-2))
print('P(x<0): %.4f' % sample_ecdf(0))
print('P(x<25): %.4f' % sample_ecdf(25))
print('P(x<50): %.4f' % sample_ecdf(50))
print('P(x<75): %.4f' % sample_ecdf(75))
print('P(x<100): %.4f' % sample_ecdf(100))
print('P(x<125): %.4f' % sample_ecdf(125))
print('P(x<140): %.4f' % sample_ecdf(140))
print('P(x<150): %.4f' % sample_ecdf(150))
(B.1)经验分布函数
上面的部分演示了如何经验地推导出一个变量的分布。由于CDF以变量为单位度量“异常性”,因此可以将其发展成为变量的单变量异常值得分。
(B.2)ECOD算法
多维数据也称为多元数据。在多维数据中,每个观测值都有多个值。一个观测值在某些维度上可能具有极端值,在其他维度上可能具有正常值。因此,一个观测值在某些维度上可能具有高的异常值得分,在其他维度上可能具有低的得分。ECOD将单变量异常值得分聚合起来,以获得观测值的整体异常值得分。
Figure (B.2): 左偏和右偏分布(作者提供的图像)
然而,在聚合单变量异常值得分时存在一个小的技术挑战——一个维度的分布可以是左偏或右偏,如图(B.2)所示。假设异常值总是出现在分布的左侧或右侧是没有意义的。我们应该首先通过确定分布是左偏还是右偏来做一个聪明的工作。在左偏分布中,均值小于众数,在右偏分布中,均值大于众数。ECOD使用分布的偏度来为一个维度分配异常值得分。如果一个分布是右偏的,异常值得分是累积分布函数(CDF),如果是左偏的,异常值得分是1减去累积分布函数或1-CDF。然后,ECOD将所有维度的单变量异常值得分聚合起来,得到一个观测值的整体异常值得分。
(B.3) ECOD和HBOS的比较
你可能已经意识到前一章中的HBOS和本章中的ECOD的概念非常相似。它们都是无监督学习方法。它们都假设变量独立以获得变量的分布。虽然HBOS推导出变量的直方图,ECOD则经验性地推导出变量的累积分布。这两种方法都没有需要调整的超参数。此外,HBOS和ECOD都是基于分布的算法。因为基于分布的方法通常速度较快,所以它们被推荐作为建模项目中的起始技术。
© 建模过程
本书建议使用步骤1、2、3的建模过程进行异常检测。它涉及模型开发、阈值确定和特征评估。
一旦在步骤1中开发了模型并分配了异常值得分,步骤2建议您绘制异常值得分的直方图以选择一个阈值。通常情况下,直方图会呈现出一个自然的切割点,如图(C.2)所示。如果在直方图中看不到自然的切割点,通常意味着特征在区分异常值方面不够有效,您需要修改特征。
(C.1) 第一步 — 构建模型
我生成了一个包含500个观测值和六个变量的模拟数据集。我将异常值的比例设置为5%,即“contamination=0.05”。我还创建了一个目标变量Y作为基本事实。然而,无监督模型只会使用X变量,Y变量仅用于验证。
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()
前几个观测值如下所示:
让我在散点图中绘制前两个变量,如图(C.1)所示。黄色点是异常值,紫色点是正常数据点。
# 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()
图(C.1)
(C.1.1) 模型
在下面我们声明并拟合模型,然后使用函数decision_functions()
为训练数据和测试数据生成异常值得分。
from pyod.models.ecod import ECOD
ecod = ECOD(contamination=0.05)
ecod.fit(X_train)
# Training data
y_train_scores = ecod.decision_function(X_train)
y_train_pred = ecod.predict(X_train)
# Test data
y_test_scores = ecod.decision_function(X_test)
y_test_pred = ecod.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:" , ecod.threshold_)
-
参数
contamination=0.05
声明异常值的百分比为5%。污染参数不影响异常值得分的计算。 -
PyOD使用给定的污染率来推导异常值得分的阈值,并应用函数
predict()
来分配标签(1或0)。 -
我在下面的代码中编写了一个简短的函数
count_stat()
来显示预测的“1”和“0”值的计数。 -
语法
.threshold_
显示了分配的污染率下的阈值。任何高于阈值的异常值得分都被视为异常值。
(C.1.2) 解释异常观测的异常得分
由于ECOD异常得分是单变量得分的总和,我们可以通过可视化单变量得分来理解为什么异常值具有高得分。在机器学习中,对于个体预测的可解释性非常重要,正如《The eXplainable A.I. with Python Examples》一书中所解释的那样。让我找出具有高异常得分的观测值,以演示如何可视化单变量得分。它告诉我观测值475和477以及其他观测值。
np.where(y_train_scores>22)
ECOD有一个特殊的函数explain_outlier()
来解释单变量的异常值。我在图(C.1)的左右两个图中绘制了单变量异常值得分。x轴是维度,y轴是单变量异常值得分。蓝色和橙色虚线是异常值得分的95和99百分位数。左图显示单变量异常值得分都在95%截止带附近,除了变量1,右图则都在95%截止带以上。这种异常值得分的可解释性是ECOD的一个合理特性。
ecod.explain_outlier(475)
ecod.explain_outlier(477)
(C.2)第二步 - 确定一个合理的阈值
在大多数情况下,我们不知道异常值的百分比。我们可以使用异常值分数的直方图来选择一个合理的阈值。阈值确定异常组的大小。如果有任何先前的知识表明异常值的百分比不应超过1%,则可以选择一个导致约1%异常值的阈值。图(D.2)显示了ECOD异常值分数的直方图。由于直方图中存在一个自然的切割点,我们可以将阈值设置为16.0。如果我们选择一个较低的阈值,异常值的计数将很高,反之亦然。
import matplotlib.pyplot as plt
plt.hist(y_train_scores, bins='auto') # arguments are passed to np.histogram
plt.title("Outlier score")
plt.show()
(C.3) 第三步 — 展示正常组和异常组的描述统计量
如第一章所述,两组之间特征的描述统计量(如均值和标准差)对于证明模型的合理性非常重要。我创建了一个名为descriptive_stat_threshold()
的简短函数,用于显示基于阈值的正常组和异常组特征的大小和描述统计量。下面我简单地使用了5%的阈值。您可以测试一系列阈值,以找到合理大小的异常组。
threshold = ecod.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)
上表显示了正常组和异常组的特征。它显示了正常组和异常组的计数和计数百分比。"Anomalous_Score"是平均异常分数。请记住,为了有效展示,请使用特征名称标记特征。该表告诉我们几个重要的结果:
-
**异常组的大小:**异常组约占5%。请记住,异常组的大小取决于阈值。如果选择较高的阈值,大小将会缩小。
-
**平均异常分数:**异常组的平均异常分数远高于正常组(22.86 > 9.40)。您不需要过多解释HBO分数。
-
**每个组中的特征统计:**上表显示异常组中的特征均值小于正常组。异常组中特征均值应该更高还是更低取决于业务应用。所有均值必须与领域知识一致。
由于我们在数据生成中有地面真实值y_test
,我们可以生成混淆矩阵来了解模型的性能。该模型表现良好,并识别出了所有的25个异常值。
def confusion_matrix(actual,pred):
Actual_pred = pd.DataFrame({'Actual': actual, 'Pred': pred})
cm = pd.crosstab(Actual_pred['Actual'],Actual_pred['Pred'])
return (cm)
confusion_matrix(y_train,y_train_pred)
(D) 由多个模型识别的异常值
在前面的章节中,我们学习了两个模型HBOS和ECOD。如果一个异常值被多个模型识别出来,那么它是异常值的可能性就会更高。在本节中,我将交叉比对这两个模型的预测结果来识别异常值。我首先复制了HBOS和ECOD模型,并生成它们的阈值。
########
# HBOS #
########
from pyod.models.hbos import HBOS
n_bins = 50
hbos = HBOS(n_bins=n_bins, contamination=0.05)
hbos.fit(X_train)
y_train_hbos_pred = hbos.labels_
y_test_hbos_pred = hbos.predict(X_test)
y_train_hbos_scores = hbos.decision_function(X_train)
y_test_hbos_scores = hbos.decision_function(X_test)
########
# ECOD #
########
from pyod.models.ecod import ECOD
clf_name = 'ECOD'
ecod = ECOD(contamination=0.05)
ecod.fit(X_train)
y_train_ecod_pred = ecod.labels_
y_test_ecod_pred = ecod.predict(X_test)
y_train_ecod_scores = ecod.decision_scores_ # raw outlier scores
y_test_ecod_scores = ecod.decision_function(X_test)
# Thresholds
[ecod.threshold_, hbos.threshold_]
我将HBOS和ECOD的预测的实际Y值以及“1”和“0”值放在一个数据框中。当我对HBOS和ECOD的预测进行交叉表分析时,发现有26个观测值被两个模型都识别为异常值。ECOD和HBOS都得出了一致的结果。
# Put the actual, the HBO score and the ECOD score together
Actual_pred = pd.DataFrame({'Actual': y_test, 'HBOS_pred': y_test_hbos_pred, 'ECOD_pred': y_test_ecod_pred})
Actual_pred.head()
pd.crosstab(Actual_pred['HBOS_pred'],Actual_pred['ECOD_pred'])
(E) HBOS算法概述
-
如果一个观测值在几乎所有变量方面都是异常值,那么该观测值很可能是异常值。
-
HBOS根据变量的直方图定义每个变量的异常值得分。
-
所有变量的异常值得分可以相加得到一个观测值的多元异常值得分。
-
由于直方图易于构建,HBOS是一种有效的无监督方法来检测异常值。
参考文献
- [1] Z. Li, Y. Zhao, X. Hu, N. Botta, C. Ionescu, and G. Chen, “ECOD: Unsupervised Outlier Detection Using Empirical Cumulative Distribution Functions,” in IEEE Transactions on Knowledge and Data Engineering, DOI: 10.1109/TKDE.2022.3159580.
异常检测系列文章导航
异常检测系列:Histogram-based Outlier Score_HBOS算法