K最近邻算法,也称为KNN或k-NN,可能是机器学习中最流行的算法之一。KNN通常用作有监督学习技术,其中提供了目标标签。KNN还可以用于计算到k个邻居的距离。因为后者不使用目标变量,一些在线资源(如scikit-learn KNN [1])将其称为无监督学习。PyOD中的KNN使用后者。它计算到k个邻居的距离,并使用距离定义异常值分数。
在本章的开头,我将花一些时间来阐明KNN如何在有监督学习或无监督学习中使用。然后我将解释KNN如何定义异常检测的异常值分数。
(A) 使用KNN作为无监督学习技术
无监督KNN方法计算观测值与其他观测值之间的欧氏距离。无监督KNN没有任何参数可以调整以提高性能。它只是计算邻居之间的距离。它执行以下步骤:
-
步骤1:对于每个数据点,计算与其他数据点的距离。
-
步骤2:按距离从小到大对数据点进行排序。
-
步骤3:选择前K个条目。
有几种选择可以计算两个数据点之间的距离。最流行的是欧氏距离。
(B) 使用KNN作为有监督学习技术
KNN算法广泛用作有监督学习环境中的分类算法。它用于预测新数据点的类别。它假设相似的同类数据点通常靠近彼此。
图(B)显示了具有蓝色类别和红色类别的数据点。如果有一个新的数据点,那么新数据点的类别应该是什么?该算法计算该数据点与其他数据点之间的距离。对于5个最近邻居,它计算红色类别和蓝色类别的数量。在图中,有4个红色类别和1个蓝色类别。该算法使用多数投票规则确定类别。新数据点被分配为红色类别。
图(B):KNN监督学习算法(作者提供的图片)
该过程可以列举如下。除了上述的步骤1到3之外,监督学习KNN还执行步骤4和5:
-
步骤4:在这些K个邻居中,计算各个类别的数量。
-
步骤5:将新数据点分配给占多数的类别。
© 异常分数是如何定义的?
由于异常值是远离邻近点的点,因此异常分数被定义为到其第k个最近邻居的距离。每个点都有一个异常分数。我们的任务是找出那些具有高异常分数的点。
PyOD中的KNN方法使用三种类型的距离度量之一作为异常分数:最大值(默认)、平均值和中位数。"最大值"使用到k个邻居的最大距离作为异常分数。"平均值"和"中位数"分别使用平均值和中位数作为异常分数。
(D) 建模过程
我采用以下建模过程进行模型开发、评估和结果解释。
-
模型开发
-
阈值确定
-
正常组和异常组的描述统计信息
步骤1:构建模型
第一步将构建模型并生成异常值分数。在第二步中,我们将选择一个阈值来将具有高异常值分数的异常观察结果与正常观察结果分开。如果有任何先前的知识表明异常值的百分比不应超过1%,则可以选择一个导致大约1%异常值的阈值。
在第三步中,我们将使用描述性统计(例如均值和标准差)对两个组进行概要分析。描述性统计表对于传达模型的合理性非常重要。如果预期异常组中某个特征的均值高于正常组的均值,并且结果与直觉相反,则应进行调查,修改或删除该特征。您应该重复执行步骤1到3,直到所有特征的描述性统计与预期一致。
(D.1) 步骤1:构建您的模型
让我们生成一些带有异常值的数据。我使用PyOD的实用函数generate_data()
生成了百分之十的异常值。请注意,尽管此模拟数据集具有目标变量Y,但无监督的KNN模型仅使用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)
# 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()
散点图中的黄色点是百分之十的异常值。紫色点是“正常”观察结果。
以下代码执行k-NN模型的计算,并将其存储为knn。请注意,在函数.fit()
中没有y。这是因为在无监督方法中忽略了y。def fit(self, X, y=None)。如果指定了y,则它变成了一个监督方法。
以下代码构建模型并对训练数据和测试数据进行评分。让我们逐行进行审查:
-
label_
:这是训练数据的标签向量。如果在训练数据上使用.predict()
,它是相同的。 -
decision_scores_
:这是训练数据的得分向量。如果在训练数据上使用.decision_functions()
,它是相同的。 -
decision_score()
:此评分函数为每个观测分配异常值分数。 -
predict()
:这是预测函数,根据分配的阈值分配1或0。 -
contamination
:这是异常值的百分比。在大多数情况下,我们不知道异常值的百分比,因此可以根据任何先前的知识分配一个值。PyOD将污染率默认为10%。此参数不影响异常值分数的计算。
from pyod.models.knn import KNN
knn = KNN(contamination=0.05)
knn.fit(X_train)
# Training data
y_train_scores = knn.decision_function(X_train)
y_train_pred = knn.predict(X_train)
# Test data
y_test_scores = knn.decision_function(X_test)
y_test_pred = knn.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:" , knn.threshold_)
让我们使用.get_params()
来查看KNN的默认参数。邻居的数量是5.0。污染率设置为5%。
knn.get_params()
(D.2) 第二步:确定一个合理的阈值
在大多数情况下,我们不知道异常值的百分比。我们可以使用异常值得分的直方图来选择一个合理的阈值。如果有任何先前的知识表明异常值为1%,则应选择一个导致大约1%异常值的阈值。图(D.2)中的异常值得分直方图建议在200.0处设置阈值,因为直方图中有一个自然的分割点。大多数数据点具有较低的异常值得分。如果我们选择1.0作为分割点,我们可以建议那些>=1.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('KNN outlier score')
plt.show()
(D.3)第三步:对正常组和异常组进行分析
对正常组和异常组进行分析是展示模型的可靠性的关键步骤。正常组和异常组的特征统计应与领域知识一致。如果异常组中某个特征的均值应该很高,但结果却相反,建议您检查、修改或丢弃该特征。直到所有特征与先前的知识一致为止,您应该迭代建模过程。另一方面,如果数据提供了新的见解,建议您验证先前的知识。
threshold = knn.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)
表格(D.3)摘要统计
上表显示了正常组和异常组的特征。它显示了正常组和异常组的计数和计数百分比。请记住,在有效的展示中,要用特征名称标记特征。该表告诉我们几个重要的结果:
-
异常组的大小: 一旦确定了阈值,大小就确定了。如果阈值是从图(D.2)中得出的,并且没有先验知识,那么大小统计数据成为一个很好的起点参考。
-
每个组中的特征统计: 所有均值必须与领域知识一致。在我们的案例中,异常组的均值小于正常组的均值。
-
平均异常分数: 异常组的平均分数应该高于正常组的分数。您不需要过多解释分数。
因为我们在数据生成中有真实值,所以我们可以生成一个混淆矩阵来了解模型的性能。该模型表现不错,并且识别出了所有的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'])
(E) 通过聚合多个模型实现模型稳定性
KNN对于k的选择非常敏感。为了产生一个稳定的模型结果,最佳实践是构建多个KNN模型,然后聚合得分。这种方法将减少过拟合的机会并提高预测准确性。
PyOD模块提供了四种方法来聚合结果。您只需要使用一种方法来产生聚合结果。
-
平均值
-
最大值的最大值(MOM)
-
最大值的平均值(AOM)
-
平均值的最大值(MOA)
我将创建20个KNN模型,k邻居的范围从10到200。
from pyod.models.combination import aom, moa, average, maximization
from pyod.utils.utility import standardizer
# 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 k-NN models.
k_list = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110,
120, 130, 140, 150, 160, 170, 180, 190, 200]
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])
train_scores.shape
# Modeling
for i in range(n_clf):
k = k_list[i]
clf = KNN(n_neighbors=k, method='largest')
clf.fit(X_train_norm)
# Store the results in each column:
train_scores[:, i] = clf.decision_scores_
test_scores[:, i] = clf.decision_function(X_test_norm)
# Decision scores have to be normalized before combination
train_scores_norm, test_scores_norm = standardizer(train_scores,test_scores)
# 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()
图(E):训练数据的平均预测直方图
图(E)显示大部分数据都在0.0以下。有一些异常值约为1.0。将阈值设置为1.0甚至2.0可能是有意义的。通过这样做,我们可以对正常组和异常组进行分析。表(E)显示了描述性统计数据。它确定了25个数据点为异常值。异常组的特征均值都小于正常组的均值。这与表(D.3)中的结果一致。
descriptive_stat_threshold(X_train,y_train_by_average, 0.5)
表格(E)
(F) 摘要
-
无监督的k-NN方法计算观测值与其他观测值之间的欧氏距离。无监督的KNN没有任何参数可以调整以提高性能。它只是计算邻居之间的距离。
-
KNN的异常值分数定义为与其第k个最近邻的距离。
附录:kNN和k-Means中的“距离”是什么?
kNN找到与最近邻的距离,而k-Means找到与簇的质心的距离。
Scikit-learn库包括有监督的kNN(参见1.6.1无监督最近邻)和K-means算法。我将只使用我们数据中的前五个数据点来展示它们之间的差异。
import numpy as np
from sklearn.neighbors import NearestNeighbors
samples = X_train[0:5]
samples
(F.1) k-NN:
无监督的k-NN计算一个点到其最近数据点的距离。例如,点‘0’的最近点(注意Numpy将这五个点标记为[0,1,2,3,4])是点4,距离为0.72002248。
my_knn = NearestNeighbors(n_neighbors=2, algorithm='ball_tree').fit(samples)
distances, indices = my_knn.kneighbors(samples, return_distance=True)
distances
(F.2) k-means:
当你指定将有N个簇时,k-Means方法会从N个随机选择的种子开始。它计算每个数据点到这N个种子的距离。然后k-means将每个数据点分配给最近的种子以形成一个簇。这将把数据点分组成N个簇。然后k-means使用簇的质心更新每个种子的位置,因此种子是簇的质心。这个过程将迭代,直到质心的值稳定下来。
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=2, random_state=0).fit(samples)
kmeans.labels_
上面的输出显示了五个点的聚类。下面的输出显示了两个聚类中心的位置。
centroids = kmeans.cluster_centers_
centroids
异常检测系列文章导航
异常检测系列:Histogram-based Outlier Score_HBOS算法