Python 离群点检测算法 -- OCSVM

Python
72
0
0
2024-09-26

分类问题通常采用监督学习算法解决,如随机森林、支持向量机、逻辑回归器等。监督学习算法需要已知目标来建立模型,但通常只能观察到正常的数据模式,而看不到罕见事件。由于罕见事件的目标数据要么不可用,要么数量不足以进行模型训练,单类支持向量机(OCSVM)可以解决只有一类数据的问题,对正常类的属性进行建模,能够检测到异常数据。本章将解释支持向量机 (SVM) 的概念,并介绍如何将其发展为单类 SVM (OCSVM),以及它是如何定义离群值的

支持向量机(SVM)

支持向量机(SVM)是一种监督学习算法,可处理分类和回归问题,由Vladimir Vapnik及其同事在1992-1995年在AT&T贝尔实验室开发。现已广泛应用于分类问题。

SVM 有一个非常巧妙的特性。它可以创建一个非线性决策边界来分离两个类别。它在高维空间中找到分离的方法非常优雅。首先将无法用直线分离的数据点投影到高维空间,然后就会出现一个 "直线 "超平面,将一个类别的数据点与另一个类别的数据点分离开来。当超平面投影回原始空间时,它将是一条非线性曲线。这可以从图 (B) 中看出。左图显示,蓝点和红点无法用任何直线分开。但如果将所有点投影到三维空间,结果就变成了线性分离。当数据投影回原始空间时,边界则是一条非线性曲线。为什么在高维空间中成分分离会变得更容易?这要追溯到瓦普尼克-切沃能基斯(VC)理论。该理论认为,映射到更高维度的空间往往能提供更强的分类能力。

SVM

SVM在高维空间中寻找支持向量,如上图所示的虚线超平面。支持向量位于特征空间中每个类别的边缘,通过最大化超平面的间隔来实现两个类别的最大分离度。除了支持向量之间的区域外,SVM还允许一些点以避免过度拟合。

从 SVM 到单类 SVM

建立算法来区分一个类和另一个类的方法之一是使用单类 SVM。这种方法将所有数据点从高维空间的原点分离出来,并将该超平面到原点的距离最大化,以此来从正常类中分离出目标类。另一种方法是使用球面进行分离,而不是超平面。

OVSVM

OCSVM 如何定义离群点得分?

OCSVM 离群点得分是数据点到超平面的距离,也称为相似度。相似度的计算方法是使用核函数如径向基函数、线性函数、多项式函数或西格玛函数计算相应的 N 维相似度矩阵之和。径向基函数简单地计算输入 x 与固定点 c 之间的距离。如

j(x)=f(‖x-c‖)

OCSVM 对 RBF 和参数的选择很敏感

OCSVM 对于内核选择和部分超参数非常敏感,这会导致不同选择下性能有很大差异。根据文献的记录,一个名为nu的重要超参数决定了数据点成为训练数据中离群点的概率。它的取值介于0和1之间。当nu为10%时,意味着10%的数据被支持边界错误地分类为离群值,也意味着10%的数据位于边界上。具体来说,nu需要在离群值和支持向量数量之间进行权衡。

由于OCSVM对超参数非常敏感,解决方法是建立多个模型,然后平均预测结果以获得更稳定的结果。在接下来的章节中,将用一系列nu值建立模型,然后对预测结果进行汇总。

建模流程

步骤 1 - 建立模型

我们将使用数据生成过程 (DGP) 模拟 500 个观测值和六个变量,其中异常值比例设定为 5%。目标变量为 Y,我们将只使用 X 数据来建立无监督模型 OCSVM。

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()

下图是前两个变量的散点图。黄色圆点为异常值,紫色圆点为正常数据点。

# 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()

下面的代码通过指定并拟合了模型 ocsvm,其中参数 contamination=0.05 表示离群值的百分比为 5%。这一参数对离群值分数的计算并没有影响。如果没有指定,PyOD 的默认值为 10%。接下来,函数 decision_function() 用于计算观测值的离群值,而函数 predict() 则根据contamination的赋值来决定输出 "1" 或 "0"。最后,语法 .threshold_ 可以显示指定contamination下的阈值。

from pyod.models.ocsvm import OCSVM
ocsvm = OCSVM(contamination=0.05)  
ocsvm.fit(X_train)

# Training data
y_train_scores = ocsvm.decision_function(X_train)
y_train_pred = ocsvm.predict(X_train)

# Test data
y_test_scores = ocsvm.decision_function(X_test)
y_test_pred = ocsvm.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:" , ocsvm.threshold_)
The training data: {0: 475, 1: 25}
The training data: {0: 475, 1: 25}
The threshold for the defined comtanimation rate: 
29.680071121036956

我们可以通过.get_params() 打印出超参数值:

ocsvm.get_params()
{'cache_size': 200,
 'coef0': 0.0,
 'contamination': 0.05,
 'degree': 3,
 'gamma': 'auto',
 'kernel': 'rbf',
 'max_iter': -1,
 'nu': 0.5,
 'shrinking': True,
 'tol': 0.001,
 'verbose': False}

OCSVM的主要参数与核函数密切相关,默认情况下使用rbf核函数,nu值为0.5。此外,核函数中的独立项coef0在polysigmoid中具有意义。对于多项式核函数(poly),degree决定了多项式函数的阶数。模型优化的最大迭代次数由max_iterint设置,默认为-1,表示在优化达到收敛之前没有限制。停止条件的容差可通过参数tol进行设置。catch_size决定了RAM的大小,从而影响了计算机RAM的使用率,默认值为200(MB)。在内存足够的情况下,可以选择将其调整为更高的值,例如500(MB)或1000(MB)。通常情况下,无需过于担心此参数。

步骤 2 - 确定合理的阈值

离群值得分衡量离群值和正常数据点的偏差,所以可以使用离群值得分的直方图来了解分布情况。直方图展示了离群值高的数据点所占的百分比,从而有助于确定合理的阈值。图 (E.2) 建议将阈值设为 16.0,因为直方图中存在一个自然切点,阈值决定了异常组的大小。

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('One-class SVM outlier score')
plt.show()

第 3 步 - 显示正常组和异常组的描述性统计结果

离群值得分衡量离群值和正常数据点的偏差,所以可以使用离群值得分的直方图来了解分布情况。直方图展示了离群值高的数据点所占的百分比,从而有助于确定合理的阈值。上图建议将阈值设为 16.0,因为直方图中存在一个自然切点,阈值决定了异常组的大小。

threshold = ocsvm.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)

模型评估中的关键指标包括计数百分比和特征均值。阈值的选择将决定离群值的数量,较高的阈值将导致离群值减少。特征均值要与领域知识保持一致,如有偏离应重新检查或删除该特征。在进行特征标注时需要有效展示。离群组的平均异常得分应高于正常组。我们可以利用混淆矩阵来评估模型性能,该模型成功识别了全部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'])

通过聚合多个模型实现模型稳定性

OCSVM是一种基于邻近度的算法,对异常值敏感且容易过拟合,特别是在第(D)节中。为了建立稳定的模型结果,应建立多个参数范围各异的模型,然后汇总预测结果。

PyOD模块提供了四种汇总结果的方法:平均值(Average)、最大值的最大值(MOM)、最大值的平均值(AOM)、平均值的最大值(MOA)。安装这些函数使用 pip install combo。请注意,只需使用一种聚合方法。另外,输入数据已经被标准化处理,但许多函数会自动进行标准化处理。

由于nu参数最敏感,因此需要建立多个 nu 值范围广泛的模型,总共会有 11 个模型。我们准备 11 列的空数据帧来存储这些模型的预测结果。

from pyod.models.combination import aom, moa, average, maximization
from pyod.utils.utility import standardizer
from pyod.models.ocsvm import OCSVM

# Standardize data
X_train_norm, X_test_norm = standardizer(X_train, X_test)

# Test a range of nu
k_list = [0.01, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.99]
n_clf = len(k_list)
# 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])

# Modeling
for i in range(n_clf):
    k = k_list[i]
    ocsvm = OCSVM(nu=k,contamination=0.05)  
    ocsvm.fit(X_train_norm)
    # Store the results in each column:
    train_scores[:, i] = ocsvm.decision_function(X_train_norm) 
    test_scores[:, i] = ocsvm.decision_function(X_test_norm) 
# Decision scores have to be normalized before combination
train_scores_norm, test_scores_norm = standardizer(train_scores,test_scores)

预测模型的十个分数存储在 "train_scores" 中,并对其进行了归一化处理,以便对十个预测结果进行平均。PyOD 模块提供了四种聚合方法,你只需选择其中一种即可得出汇总结果。

# Combination by average
# The test_scores_norm is 500 x 10. The "average" function will take the average of the 10 columns. The result "y_by_average" is a single column: 
y_train_by_average = average(train_scores_norm)
y_test_by_average = average(test_scores_norm)
import matplotlib.pyplot as plt
plt.hist(y_train_by_average, bins='auto') # arguments are passed to np.histogram
plt.title("Combination by average")
plt.show()

训练数据平均预测值直方图

图表显示阈值为1.40,根据总分可得到描述性统计,发现有25个数据点为异常值。读者可以对表(D.3)进行类似的解释。

descriptive_stat_threshold(
      X_train,y_train_by_average, 1.4)

OCSVMA 算法总结

OCSVM根据正常类的属性建立模型,以检测非正常类数据。它在高维空间中将数据点与原点分离,并最大化该超平面到原点的距离。换句话说,原点就是算法试图从正常类中分离出来的类。