异常值检测

Task01:

今天开始了异常值学习的第一天。我在本科阶段学习过一些关于高维数据流故障诊断的知识。当时主要学习的是大规模假设检验,或许这也算是异常值检验的一种?今天,我跟随着Datawhale团队的步伐,学习数据挖掘中的异常值检验。

1.1 异常值检验的类型:

1.1.1 点异常:指的是少数个体是异常的,大多数个体是正常的。说实话,之前自己理解的数据挖掘中的异常值,就是点异常,自己还从未学习过下面两种异常以及相应的检验方法。

1.1.2 上下文异常:指的是在特定环境下个体是异常的,在其他环境下都是正常的。这种异常情况让我感到很新鲜,我期待学习这种异常的处理方式。

1.1.3 群体异常:指的是在群体集合中的个体出现异常,而该个体本身可能并不异常。这种情况仔细一想,感觉在生活中确实挺常见的,但是感觉处理起来会比较难?

1.2 异常值检验的常见方法:

1.2.1 传统的方法:

包括基于统计学的方法(主要是建立合适的统计模型)、基于线性模型的方法(PCA)、基于相似度的方法(主要是聚类方法)。

1.2.2 基于集成学习的方法:

feature bagging 以及孤立森林

1.2.3 基于机器学习的方法

1.3 重要python库PyOD


Task02:基于统计方法的异常值检测

个人认为基于统计学的方法就分为参数方法和非参方法。

2.1 参数方法:

参数方法主要就是假定原数据服从正态分布,并使用最大似然估计从样本中估计出正态分布的均值和方差。最后,根据 3-sigma 原则对新来的数据进行故障诊断。

2.2 非参数方法:

非参数方法主要是利用直方图去判断故障。其中比较常见的方法就是HBOS算法。HBOS算法主要分为两种:

静态宽度直⽅图:标准的直⽅图构建⽅法,在值范围内使⽤k个等宽箱。样本落⼊每个桶的频率(相对数量)作为密度(箱⼦⾼度)的估计。

动态宽度直⽅图:⾸先对所有值进⾏排序,然后固定数量的\frac{N}{k} 个连续值装进⼀个箱⾥,其 中N是总实例数,k是箱个数;直⽅图中的箱⾯积表⽰实例数。因为箱的宽度是由箱中第⼀个值和最后⼀个值决定的,所有箱的⾯积都⼀样,因此每⼀个箱的⾼度都是可计算的。这意味着跨度⼤的箱的⾼度低,即密度小,只有⼀种情况例外,超过k个数相等,此时允许在同⼀个箱⾥超过\frac{N}{k} 值。

代码:

由于我是个python小白,此处的代码是跟着 https://www.analyticsvidhya.com/blog/2019/02/outlier-detection-python-pyod/网站上的代码练习的,没有改动。唯一的变动时我把他中间的ABOD算法改成了HBOS算法,KNN算法保持不变。代码如下:

···

import numpyas np

from scipyimport stats

import matplotlib.pyplotas plt

import matplotlib.font_manager

from pyod.models.hbosimport HBOS

from pyod.models.knnimport KNN

from pyod.utils.dataimport generate_data, get_outliers_inliers

#generate random data with two features

X_train, Y_train = generate_data(n_train=200,train_only=True, n_features=2)

# by default the outlier fraction is 0.1 in generate data function

outlier_fraction =0.1

# store outliers and inliers in different numpy arrays

x_outliers, x_inliers = get_outliers_inliers(X_train,Y_train)

n_inliers =len(x_inliers)

n_outliers =len(x_outliers)

#separate the two features and use it to plot the data

F1 = X_train[:,[0]].reshape(-1,1)

F2 = X_train[:,[1]].reshape(-1,1)

# create a meshgrid

xx, yy = np.meshgrid(np.linspace(-10, 10, 200), np.linspace(-10, 10, 200))

# scatter plot

plt.scatter(F1,F2)

plt.xlabel('F1')

plt.ylabel('F2')

plt.show()

classifiers = {

'HBOS (ABOD)'  : HBOS(contamination=outlier_fraction),

    'K Nearest Neighbors (KNN)' :  KNN(contamination=outlier_fraction)

}

#set the figure size

plt.figure(figsize=(10, 10))

for i, (clf_name,clf)in enumerate(classifiers.items()) :

# fit the dataset to the model

    clf.fit(X_train)

# predict raw anomaly score

    scores_pred = clf.decision_function(X_train)*-1

    # prediction of a datapoint category outlier or inlier

    y_pred = clf.predict(X_train)

# no of errors in prediction

    n_errors = (y_pred != Y_train).sum()

print('No of Errors : ',clf_name, n_errors)

# rest of the code is to create the visualization

# threshold value to consider a datapoint inlier or outlier

    threshold = stats.scoreatpercentile(scores_pred,100 *outlier_fraction)

print("threshold:",threshold)

# decision function calculates the raw anomaly score for every point

    Z = clf.decision_function(np.c_[xx.ravel(), yy.ravel()]) * -1

    Z = Z.reshape(xx.shape)

subplot = plt.subplot(1, 2, i +1)

# fill blue colormap from minimum anomaly score to threshold value

    subplot.contourf(xx, yy, Z, levels = np.linspace(Z.min(), threshold, 10),cmap=plt.cm.Blues_r)

# draw red contour line where anomaly score is equal to threshold

    a = subplot.contour(xx, yy, Z, levels=[threshold],linewidths=2, colors='red')

# fill orange contour lines where range of anomaly score is from threshold to maximum anomaly score

    subplot.contourf(xx, yy, Z, levels=[threshold, Z.max()],colors='orange')

# scatter plot of inliers with white dots

    b = subplot.scatter(X_train[:-n_outliers, 0], X_train[:-n_outliers, 1], c='white',s=20, edgecolor='k')

# scatter plot of outliers with black dots

    c = subplot.scatter(X_train[-n_outliers:, 0], X_train[-n_outliers:, 1], c='black',s=20, edgecolor='k')

subplot.axis('tight')

subplot.legend(

[a.collections[0], b, c],

        ['learned decision function', 'true inliers', 'true outliers'],

        prop=matplotlib.font_manager.FontProperties(size=10),

        loc='lower right')

subplot.set_title(clf_name)

subplot.set_xlim((-10, 10))

subplot.set_ylim((-10, 10))

plt.show()

···

·Task03:线性模型

第三个task主要讨论了两种线性模型,其一是回归模型,其二是PCA方法。下面一一进行叙述。个人认为,除非有特殊要求,二者在进行计算前都应该使用归一化,以消除量纲的影响。

回归模型:

回归模型是统计学最常见的模型之一,最常见的就是在二次损失函数下的回归模型,通常我们会用最小二乘法去解决这类问题。当然,梯度下降法在这类问题中也有着不错的表现。我们可以从一个图来理解回归在异常检测中的作用(图摘自:https://blog.csdn.net/ice_night/article/details/109675564)


可以看出,这是一个最简单的二维例子,当我们用正常点建立绿色的回归直线时,右上方的异常值点是很容易被识别出来的。当维数上升,我们可以建立回归超平面,之后根据每个点到超平面的距离来当做其的异常值得分,进而识别异常值点。


PCA:

PCA,即主成分分析,是一种常见的降维工具。PCA的原理是通过构造一个新的特征空间,把原数据映射到这个新的低维空间里。PCA做特征值分解后得到的特征值较大的方向,其方差也较大;特征值较小的方向,方差也较小。每个点的异常值得分如下:

Score(X)=\sum_{j=1}^d \frac{|(X-\mu )e_{j}|^{2}}{\lambda _{j}}

其中,e_{j}为第j个特征向量,\lambda_{j}为第j个特征值。可以看出,特征值越小(方差越小),异常值得分越大。这一点是合理的,因为在方差小的方向如果出现偏移较大的现象,显然更应该被识别成异常值点。

代码:

代码来自于datawhale团队提供的pdf。由于本人还是python小白,只是跟着pdf的顺序熟悉了一下流程。

···

import warnings

warnings.filterwarnings('ignore')

import pandasas pd

import numpyas np

import matplotlib.pyplotas plt

import seabornas sns

import missingnoas msno

path ='D:\Wisconsin\python_learn\pandas\sample_data/'

f=open(path+'breast-cancer-unsupervised-ad.csv')

Train_data = pd.read_csv(f,header =None)

print([columnfor columnin Train_data])

a = {i:'f' +str(i)for iin range(30)}

a[30]='label'

print(a)

Train_data.rename(columns=a,inplace=True)

print(Train_data)

numeric_features = ['f' +str(i)for iin range(30)]

## 1) 相关性分析

numeric = Train_data[numeric_features]

correlation = numeric.corr()

f, ax = plt.subplots(figsize = (14, 14))

sns.heatmap(correlation,square =True)

plt.title('Correlation of Numeric Features with Price',y=1,size=16)

plt.show()

from sklearn.manifoldimport TSNE

tsne = TSNE(n_components=2, init='pca', random_state=0)

result = tsne.fit_transform(numeric)

x_min, x_max = np.min(result, 0), np.max(result, 0)

result = (result - x_min) / (x_max - x_min)

label = Train_data['label']

fig = plt.figure(figsize = (7, 7))

#f , ax = plt.subplots()

color = {'o':0, 'n':7}

for iin range(result.shape[0]):

plt.text(result[i, 0], result[i, 1], str(label[i]),

            color=plt.cm.Set1(color[label[i]] /10.),

            fontdict={'weight':'bold', 'size':9})

plt.xticks([])

plt.yticks([])

plt.title('Visualization of data dimension reduction')

plt.show()

···

Task04: 基于相似度的方法

这次任务介绍了基于相似度的方法。个人学习后感觉这主要分为两个方面,其一是从距离去刻画相似度,一个是从密度去刻画相似度。

4.1 .基于距离

基于距离的方法最简单的是直接各个点之间的举例,通常采用嵌套循环的方法。但这种方法时间复杂度较高。因此,我们往往采用表格法(图摘自datawhale团队的pdf)。其中L1是离选定单元格只有一个单位距离的单元格,L2是离选定单元格有两个或三个单位距离的单元格。



判定规则如下:

1. 如果一个单元格中包含超过k个数据点及其L1邻居,那么这些数据点都不是异常值。

2. 如果一个单元格及其L1、L2邻居包含的额数值点少于k个,那么该单元格内的数据都是异常值。

4.2 基于密度的方法

局部离群点因子是一种基于密度识别局部离群点的算法,由 Breuning et al.(2000) 提出。主要思想是:通过某数据点与其 k 个邻居的距离来定义局部密度。再将数据点的局部密度与其 k 个邻居的局部密度作比较。若该数据点的局部密度相对较小则为离群点。

需要用到的一些相关定义:(参考https://blog.csdn.net/wangyibo0201/article/details/51705966)

1. d(p,o):两点p和o之间的距离;

 2. k-distance对于点p的第k距离dk(p)定义如下:

   dk(p)=d(p,o),并且满足:

      a) 在集合中至少有不包括p在内的kk个点o,∈C{x≠p}, 满足d(p,o,)≤d(p,o);

      b) 在集合中最多有不包括p在内的k−1k−1个点o,∈C{x≠p},满足d(p,o,)<d(p,o)d(p,o,)

           p的第k距离,也就是距离p第k远的点的距离,不包括p.

3. k-distance neighborhood of p 

  点p的第k距离邻域Nk(p),就是p的第k距离即以内的所有点,包括第k距离。

      因此p的第k邻域点的个数 |Nk(p)|≥k|。

4. reach distance

    点o到点p的第k可达距离定义为:

    reach−distancek(p,o)=max{k−distance(o),d(p,o)}

 也就是,点o到点p的第k可达距离,至少是o的第k距离,或者为o、p间的真实距离。

 这也意味着,离点o最近的k个点,o到它们的可达距离被认为相等,且都等于dk(o)。

5.局部可达密度:

lrd_{k}(p)=1/(\frac{\sum_{o\in N_{k}(p)}reach-dist_{k}(p,o)}{|N_{k}(p)|})

6. LOF

LOF_{k}(p)=\frac{\sum_{o\in N_{k}(p)}\frac{lrd_{k}(o)}{lrd_{k}(p)}}{|N_{k}(p)|}


代码:(来自datawhale团队)

···

import numpy as np

import pandas as pd

import matplotlib.pyplot as plt

from sklearn.neighbors import LocalOutlierFactor

plt.rcParams['font.sans-serif'] = ['SimHei']

plt.rcParams['axes.unicode_minus']=False

pd.set_option('display.max_columns', None)

pd.set_option('display.max_rows', None)

np.random.seed(61)

# 构造两个数据点集群

X_inliers1 = 0.2 * np.random.randn(100, 2)

X_inliers2 = 0.5 * np.random.randn(100, 2)

X_inliers = np.r_[X_inliers1 + 2, X_inliers2 - 2]

# 构造⼀些离群的点

X_outliers = np.random.uniform(low=-4, high=4, size=(20, 2))

# 拼成训练集

X = np.r_[X_inliers, X_outliers]

n_outliers = len(X_outliers)

ground_truth = np.ones(len(X), dtype=int)

# 打标签,群内点构造离群值为1,离群点构造离群值为-1

ground_truth[-n_outliers:] = -1

plt.title('构造数据集 (LOF)')

plt.scatter(X[:-n_outliers, 0], X[:-n_outliers, 1], color='b', s=5, label='集群点')

plt.scatter(X[-n_outliers:, 0], X[-n_outliers:, 1], color='orange', s=5,

label='离群点')

plt.axis('tight')

plt.xlim((-5, 5))

plt.ylim((-5, 5))

legend = plt.legend(loc='upper left')

legend.legendHandles[0]._sizes = [10]

legend.legendHandles[1]._sizes = [20]

plt.show()

# 训练模型(找出每个数据的实际离群值)

clf = LocalOutlierFactor(n_neighbors=20, contamination=0.1)

# 对单个数据集进⾏⽆监督检测时,以1和-1分别表⽰⾮离群点与离群点

y_pred = clf.fit_predict(X)

# 找出构造离群值与实际离群值不同的点

n_errors = y_pred != ground_truth

X_pred = np.c_[X,n_errors]

X_scores = clf.negative_outlier_factor_

# 实际离群值有正有负,转化为正数并保留其差异性(不是直接取绝对值)

X_scores_nor = (X_scores.max() - X_scores) / (X_scores.max() - X_scores.min())

X_pred = np.c_[X_pred,X_scores_nor]

X_pred = pd.DataFrame(X_pred,columns=['x','y','pred','scores'])

X_pred_same = X_pred[X_pred['pred'] == False]

X_pred_different = X_pred[X_pred['pred'] == True]

plt.title('局部离群因⼦检测 (LOF)')

plt.scatter(X[:-n_outliers, 0], X[:-n_outliers, 1], color='b', s=5, label='集群点')

plt.scatter(X[-n_outliers:, 0], X[-n_outliers:, 1], color='orange', s=5,label='离群点')

# 以标准化之后的局部离群值为半径画圆,以圆的⼤小直观表⽰出每个数据点的离群程度

plt.scatter(X_pred_same.values[:,0], X_pred_same.values[:, 1],

s=1000 * X_pred_same.values[:, 3], edgecolors='c',facecolors='none', label='标签⼀致')

plt.scatter(X_pred_different.values[:, 0], X_pred_different.values[:, 1],s=1000 * X_pred_different.values[:, 3], edgecolors='violet',facecolors='none', label='标签不同')

plt.axis('tight')

plt.xlim((-5, 5))

plt.ylim((-5, 5))

legend = plt.legend(loc='upper left')

legend.legendHandles[0]._

sizes = [10]

legend.legendHandles[1]._

sizes = [20]

plt.show()

···

Task 05:高维数据的异常检测

高维数据的检验可以使用大规模假设检验的方法。但在这一部分,主要介绍的是feature bagging和孤立森林算法。

5.1 Feature Bagging

feature bagging算法发源于bagging算法。最大的不同就是预测对象是feature,这也是该算法名字的由来。feature bagging算法的个体学习器主要采用LOF算法,这在task 04中有所介绍。除此之外,k近邻算法也可以作为个体学习器。

feature bagging的算法的算法步骤如下:(摘自datawhale团队的pdf)


同时我们应该注意到,不同检测器在产生分数的范围可能不同。另外,有的检测器输出较⼤的异常值分数,但有些检测器会输出较小的异常值分数。因此,需要将来⾃各种检测器的分数转换成可以有意义的组合的归⼀化值。分数标准化之后,还要选择⼀个组合函数将不同基本检测器的得分进⾏组合,最常⻅的选择包括平均和最⼤化组合函数。经过这一系列的操作之后,我们便可以顺利地使用feature bagging算法去检测高位数据。

5.2 孤立森林算法

孤立森林算饭是由周志华老师在2008年提出的。其核心思想便是运用一个超平面去随机分割数据空间。循环上述操作,直到每个⼦空间只有⼀个数据点为⽌。孤⽴森林检测异常的假设是:异常点⼀般都是⾮常稀有的,在树中会很快被划分到叶⼦节点,因此可以⽤叶⼦节点到根节点的路径⻓度来判断⼀条记录是否是异常的。直观上来讲,那些具有⾼密度的簇需要被切很多次才会将其分离,而那些低密度的点很快就被单独分配到⼀个⼦空间。孤⽴森林认为这些很快被孤⽴的点就是异常点。树的构造⽅法和随机森林(random forests)中树的构造⽅法有些类似。流程如下:(摘自datawhale团队pdf)

1) 从训练数据中随机选择⼀个样本⼦集,放⼊树的根节点;

2) 随机指定⼀个属性,随机产⽣⼀个切割点V,即属性A的最⼤值和最小值之间的某个数;

3) 根据属性A对每个样本分类,把A小于V的样本放在当前节点的左叶⼦中,⼤于等于V的样本放在右也⼦中,这样就形成了2个⼦空间;

4) 在叶⼦节点中递归步骤2和3,不断地构造左孩⼦和右孩⼦,直到孩⼦节点中只有⼀个数据,或树的⾼度达到了限定⾼度。获得t棵树之后,孤⽴森林的训练就结束,就可以⽤⽣成的孤⽴森林来评估测试数据。

推荐阅读更多精彩内容