Python 离群值检测算法 -- XGBOD

Python
18
0
0
2024-09-26

监督学习用于识别已知的异常值,而无监督学习则可以探索新类型的异常值。是否可以将无监督学习的离群值作为有监督学习的特征,以充分利用两种方法的优势?这个想法涉及到表征学习,一种发现数据表征特征的机器学习方法。后续将介绍 XGBOD 监督学习技术,并探讨其他表征学习变体,如 BORE。

表征学习

表征学习是机器学习中的一门学科,研究在没有人工干预的情况下发现原始数据表征的系统方法。其目的是利用机器学习算法学习数据中的正常和模糊模式,并用新的特征表示原始数据。无监督学习中的离群值可以作为有监督学习模型的输入特征,BORE方法提出了这一观点。利用离群值分数进行监督学习可以提供更好的预测结果。

不同类型的异常值

在讨论监督学习之前,我们要先了解一下异常值的不同类型,它们在二元分类模型中通常会被标记为 "1"。医疗保险和医疗补助是美国的两项政府计划,涉及医疗和健康相关服务的覆盖。斯帕罗(2019)在其著作中描述了医疗欺诈的不同类型:医生和患者串通一气提交多份报销申请、不诚实的医疗服务提供者为虚假病人制造账单,以及涉及患者、医生、律师和医疗供应商的犯罪团伙。在数据科学的术语中,这些可以看作是不同类型的异常值。将索赔作为数据点绘制在二维图上,这些异常值可能就是图(A)中与正确账单不同的点O1、O2、a1a2。这个预测问题可以表述为一个二元分类问题,其中所有类型的异常点均为 "1",其余为 "0"。

异常类型

XGBOD

监督学习方法可以是任何分类模型,例如逻辑回归和XGBoost。XGBoost比其他集合方法更能处理不平衡数据,对于极度不平衡的目标具有吸引力。XGBoosting(EXtreme Gradient Boosting)算法是梯度提升树算法的著名实现,在其损失函数中内置了正则化形式,从而减轻了过拟合。XGBoost的并行处理和优化计算也吸引了许多数据科学家。

XGBOD包括三个步骤。

  • 首先,使用无监督学习创建新特征“变换离群点分数”(TOS)。
  • 然后,将新特征与原始特征连接,并应用皮尔逊相关系数以保留有用的特征。
  • 最后,使用XGBoost分类器进行训练。XGBoost可用于对特征进行剪枝并提供特征重要性排序。

在生成TOS时,默认情况下,XGBOD使用KNN、AvgKNN、LOF、iForest、HBOS和OCSVM。该方法列表非常广泛,但并非完全详尽。不同超参数的模型可以生成多个TOS。以下是默认模型及其超参数范围。

  • KNN、AvgKNN、LOF:KNN、AvgKNN 和 LOF 的 n_neighbors 预定义范围是 [1、3、5、10、20、30、40、50]。
  • iForest:估计器数量的预定义范围是 [10、20、50、70、100、150、200]
  • HBOS:预定义的分仓范围是 [5、10、15、20、25、30、50]
  • OCSVM:nu 的预定义范围是 [0.01、0.1、0.2、0.3、0.4、0.5、0.6、0.7、0.8、0.9、0.99]。

建模程序

在无监督学习方法中,使用了 1-2-3 步骤的建模程序:(1) 建立模型;;(2) 确定阈值;(3) 分析正常组和异常组。在 XGBOD 中,我们可以直接跳到这一步,因为目标已知。两组之间特征的描述性统计(如均值和标准差)对于说明模型的合理性非常重要。如果结果与直觉相反,就需要调查、修改或放弃该特征,并重复模型,直到所有特征都有合理的解释为止。

步骤 1 - 建立模型

为训练数据和测试数据分别生成六个变量和 500 个观测值。离群值的百分比由contamination设定为 5%。

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)

# Make the 2d numpy array a pandas dataframe for each manipulation 
X_train_pd = pd.DataFrame(X_train)
    
# 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()

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

使用decision_functions()函数为 X_trainX_test 中的每个观测值分配异常得分。

from pyod.models.xgbod import XGBOD
xgbod = XGBOD(n_components=4,random_state=100) 
xgbod.fit(X_train,y_train)

# get the prediction labels and outlier scores of the training data
y_train_pred = xgbod.labels_  # binary labels (0: inliers, 1: outliers)
y_train_scores = xgbod.decision_scores_  # raw outlier scores
y_train_scores = xgbod.decision_function(X_train)
# get the prediction on the test data
y_test_pred = xgbod.predict(X_test)  # outlier labels (0 or 1)
y_test_scores = xgbod.decision_function(X_test)  # outlier scores

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 test data:", count_stat(y_test_pred))
The training data: {0.0: 475, 1.0: 25}
The training data: {0.0: 475, 1.0: 25}

因为有测试数据的基本事实,所以可以验证模型的可预测性。混淆矩阵中显示模型正确识别了 25 个数据点,只遗漏了一个数据点。

Actual_pred = pd.DataFrame({'Actual': y_test, 'Pred': y_test_pred})
pd.crosstab(Actual_pred['Actual'],Actual_pred['Pred'])

在XGBOD中,表征学习至关重要,它应用无监督学习来创建变换离群值(TOS)。可以使用.get_params()来查看XGBOD的设置,输出包括KNN、AvgKNN、LOF、IForest、HBOS和OCSVM的规格。这些无监督学习模型中的每一个都将TOS创建为新特征,供XGBOD添加到原始特征中以构建模型。另外,输出也会包括极端梯度提升的超参数,例如,XGBoost模型的学习率为0.1,树的最大深度为3,有100棵提升树。

xgbod.get_params()

上下滑动查看更多

{'base_score': 0.5,
'booster': 'gbtree',
'colsample_bylevel': 1,
'colsample_bytree': 1,
'estimator_list': [KNN(algorithm='auto', contamination=0.1, leaf_size=30, method='largest',
metric='minkowski', metric_params=None, n_jobs=1, n_neighbors=1, p=2,
radius=1.0),
LOF(algorithm='auto', contamination=0.1, leaf_size=30, metric='minkowski',
metric_params=None, n_jobs=1, n_neighbors=1, novelty=True, p=2),
KNN(algorithm='auto', contamination=0.1, leaf_size=30, method='largest',
metric='minkowski', metric_params=None, n_jobs=1, n_neighbors=3, p=2,
radius=1.0),
LOF(algorithm='auto', contamination=0.1, leaf_size=30, metric='minkowski',
metric_params=None, n_jobs=1, n_neighbors=3, novelty=True, p=2),
KNN(algorithm='auto', contamination=0.1, leaf_size=30, method='largest',
metric='minkowski', metric_params=None, n_jobs=1, n_neighbors=5, p=2,
radius=1.0),
LOF(algorithm='auto', contamination=0.1, leaf_size=30, metric='minkowski',
metric_params=None, n_jobs=1, n_neighbors=5, novelty=True, p=2),
KNN(algorithm='auto', contamination=0.1, leaf_size=30, method='largest',
metric='minkowski', metric_params=None, n_jobs=1, n_neighbors=10, p=2,
radius=1.0),
LOF(algorithm='auto', contamination=0.1, leaf_size=30, metric='minkowski',
metric_params=None, n_jobs=1, n_neighbors=10, novelty=True, p=2),
KNN(algorithm='auto', contamination=0.1, leaf_size=30, method='largest',
metric='minkowski', metric_params=None, n_jobs=1, n_neighbors=20, p=2,
radius=1.0),
LOF(algorithm='auto', contamination=0.1, leaf_size=30, metric='minkowski',
metric_params=None, n_jobs=1, n_neighbors=20, novelty=True, p=2),
KNN(algorithm='auto', contamination=0.1, leaf_size=30, method='largest',
metric='minkowski', metric_params=None, n_jobs=1, n_neighbors=30, p=2,
radius=1.0),
LOF(algorithm='auto', contamination=0.1, leaf_size=30, metric='minkowski',
metric_params=None, n_jobs=1, n_neighbors=30, novelty=True, p=2),
KNN(algorithm='auto', contamination=0.1, leaf_size=30, method='largest',
metric='minkowski', metric_params=None, n_jobs=1, n_neighbors=40, p=2,
radius=1.0),
LOF(algorithm='auto', contamination=0.1, leaf_size=30, metric='minkowski',
metric_params=None, n_jobs=1, n_neighbors=40, novelty=True, p=2),
KNN(algorithm='auto', contamination=0.1, leaf_size=30, method='largest',
metric='minkowski', metric_params=None, n_jobs=1, n_neighbors=50, p=2,
radius=1.0),
LOF(algorithm='auto', contamination=0.1, leaf_size=30, metric='minkowski',
metric_params=None, n_jobs=1, n_neighbors=50, novelty=True, p=2),
HBOS(alpha=0.1, contamination=0.1, n_bins=5, tol=0.5),
HBOS(alpha=0.1, contamination=0.1, n_bins=10, tol=0.5),
HBOS(alpha=0.1, contamination=0.1, n_bins=15, tol=0.5),
HBOS(alpha=0.1, contamination=0.1, n_bins=20, tol=0.5),
HBOS(alpha=0.1, contamination=0.1, n_bins=25, tol=0.5),
HBOS(alpha=0.1, contamination=0.1, n_bins=30, tol=0.5),
HBOS(alpha=0.1, contamination=0.1, n_bins=50, tol=0.5),
OCSVM(cache_size=200, coef0=0.0, contamination=0.1, degree=3, gamma='auto',
kernel='rbf', max_iter=-1, nu=0.01, shrinking=True, tol=0.001,
verbose=False),
OCSVM(cache_size=200, coef0=0.0, contamination=0.1, degree=3, gamma='auto',
kernel='rbf', max_iter=-1, nu=0.1, shrinking=True, tol=0.001,
verbose=False),
OCSVM(cache_size=200, coef0=0.0, contamination=0.1, degree=3, gamma='auto',
kernel='rbf', max_iter=-1, nu=0.2, shrinking=True, tol=0.001,
verbose=False),
OCSVM(cache_size=200, coef0=0.0, contamination=0.1, degree=3, gamma='auto',
kernel='rbf', max_iter=-1, nu=0.3, shrinking=True, tol=0.001,
verbose=False),
OCSVM(cache_size=200, coef0=0.0, contamination=0.1, degree=3, gamma='auto',
kernel='rbf', max_iter=-1, nu=0.4, shrinking=True, tol=0.001,
verbose=False),
OCSVM(cache_size=200, coef0=0.0, contamination=0.1, degree=3, gamma='auto',
kernel='rbf', max_iter=-1, nu=0.5, shrinking=True, tol=0.001,
verbose=False),
OCSVM(cache_size=200, coef0=0.0, contamination=0.1, degree=3, gamma='auto',
kernel='rbf', max_iter=-1, nu=0.6, shrinking=True, tol=0.001,
verbose=False),
OCSVM(cache_size=200, coef0=0.0, contamination=0.1, degree=3, gamma='auto',
kernel='rbf', max_iter=-1, nu=0.7, shrinking=True, tol=0.001,
verbose=False),
OCSVM(cache_size=200, coef0=0.0, contamination=0.1, degree=3, gamma='auto',
kernel='rbf', max_iter=-1, nu=0.8, shrinking=True, tol=0.001,
verbose=False),
OCSVM(cache_size=200, coef0=0.0, contamination=0.1, degree=3, gamma='auto',
kernel='rbf', max_iter=-1, nu=0.9, shrinking=True, tol=0.001,
verbose=False),
OCSVM(cache_size=200, coef0=0.0, contamination=0.1, degree=3, gamma='auto',
kernel='rbf', max_iter=-1, nu=0.99, shrinking=True, tol=0.001,
verbose=False),
IForest(behaviour='old', bootstrap=False, contamination=0.1, max_features=1.0,
max_samples='auto', n_estimators=10, n_jobs=1, random_state=100,
verbose=0),
IForest(behaviour='old', bootstrap=False, contamination=0.1, max_features=1.0,
max_samples='auto', n_estimators=20, n_jobs=1, random_state=100,
verbose=0),
IForest(behaviour='old', bootstrap=False, contamination=0.1, max_features=1.0,
max_samples='auto', n_estimators=50, n_jobs=1, random_state=100,
verbose=0),
IForest(behaviour='old', bootstrap=False, contamination=0.1, max_features=1.0,
max_samples='auto', n_estimators=70, n_jobs=1, random_state=100,
verbose=0),
IForest(behaviour='old', bootstrap=False, contamination=0.1, max_features=1.0,
max_samples='auto', n_estimators=100, n_jobs=1, random_state=100,
verbose=0),
IForest(behaviour='old', bootstrap=False, contamination=0.1, max_features=1.0,
max_samples='auto', n_estimators=150, n_jobs=1, random_state=100,
verbose=0),
IForest(behaviour='old', bootstrap=False, contamination=0.1, max_features=1.0,
max_samples='auto', n_estimators=200, n_jobs=1, random_state=100,
verbose=0)],
'gamma': 0,
'learning_rate': 0.1,
'max_delta_step': 0,
'max_depth': 3,
'min_child_weight': 1,
'n_estimators': 100,
'n_jobs': 1,
'nthread': None,
'objective': 'binary:logistic',
'random_state': 100,
'reg_alpha': 0,
'reg_lambda': 1,
'scale_pos_weight': 1,
'silent': True,
'standardization_flag_list': [True,
True,
True,
True,
True,
True,
True,
True,
True,
True,
True,
True,
True,
True,
True,
True,
False,
False,
False,
False,
False,
False,
False,
True,
True,
True,
True,
True,
True,
True,
True,
True,
True,
True,
False,
False,
False,
False,
False,
False,
False],
'subsample': 1}
步骤 2 - 正常组和异常组的描述性统计

两组之间特征的描述性统计(如均值和标准差)对于证明模型的合理性非常重要。

# Let's see how many '0's and '1's.
df_train = pd.DataFrame(X_train)
df_columns = df_train.columns
df_train['pred'] = y_train_pred
df_train['Group'] = np.where(df_train['pred']==1, 'Outlier','Normal')

# Now let's show the summary statistics:
cnt = df_train.groupby('Group')['pred'].count().reset_index().rename(columns={'pred':'Count'})
cnt['Count %'] = (cnt['Count'] / cnt['Count'].sum()) * 100 # The count and count %
stat = df_train.groupby('Group').mean().reset_index() # The avg.
cnt.merge(stat, left_on='Group',right_on='Group') # Put the count and the avg. together

表格显示了正常组和离群组的计数和计数百分比。重要的结果包括:

  • 异常值组的大小: 离群组大约占总体的10%。离群组的大小由阈值决定,阈值越大,离群值越小。
  • 各组中的特征统计数据: 从表格中可以观察到,在离群值组中,特征"0"到"5"的值都小于正常值组。在实际业务中,可能希望离群组的特征值高于或低于正常组的特征值。因此,特征统计有助于理解模型结果。

XGBOD总结

表征学习是一种系统方法,用于在没有人工干预的情况下发现原始数据表征。XGBOD应用不同的无监督离群点检测来创建新的特征,称为变换离群点分数(TOS),并使用皮尔逊相关系数来保留有用的特征。默认的无监督学习模型包括KNN、AvgKNN、LOF、iForest、HBOS和OCSVM,而XGBOD将TOS添加到原始特征中以建立模型。