自动编码器模型是神经网络或深度学习的重要应用。它们广泛应用于降维、图像压缩、图像去噪和特征提取。它们还应用于异常检测,并取得了优秀的结果。
深度学习是机器学习的一个完整领域。鉴于并非所有读者都熟悉深度学习,我在本章中专门介绍了深度学习的基础知识。因为很多读者熟悉回归,所以我将从回归的角度介绍深度学习,并使用逻辑回归来解释神经网络图。这种回归友好的方法可以帮助读者理解神经网络建模。
在此基础上,我将解释自动编码器的结构,并向您展示如何构建异常分数。我将深入介绍深度学习模型的组成部分,包括批量大小的概念、L1和L2正则化、迭代次数、深度学习模型中的优化器等等。完成本章后,读者将对调整超参数的一般深度学习框架感到自信。
(A) 理解深度学习
通常深度学习或神经网络会用它们的术语来进行介绍。学习者会通过脑部解剖学来“想象”它在大脑中的功能。学习者会了解神经元、相互连接和复杂的神经网络系统。在我从回归过渡到深度学习的讲座中,我有一种沉默的时刻,就像跳过了一个深深的鸿沟。术语的变化也造成了知识上的鸿沟。为了以回归友好的方式介绍深度学习,让我先解释神经元、激活函数、层、优化器等等。然后我将向您展示如何在深度学习框架中构建逻辑回归。
(A.1) 在深度学习中,数据被称为“张量”
在 y = XB + e 的回归公式中,y 是一个一维向量,X 是一个二维矩阵。在 EXCEL 电子表格中,因变量 y 是单列,协变量 X 是多列。在深度学习术语中,一列被称为张量。一个一维向量是一个张量,一个二维矩阵是一个二维张量。流行的机器学习平台 TensorFlow(tensorflow.org)也是以此命名,并用于神经网络建模。一旦您知道“张量”只是一个一维向量,您可能会感到宽慰。为什么他们不直接称之为“向量”呢?这个术语从哪里来?它来自拉丁语的“tensor”,意思是“拉伸的东西”。数学家沃尔德马尔·福伊特(1898年)利用这个概念在数学中将向量称为张量。
(A.2)输入层中的神经元是输入变量
在神经网络中,术语神经元和层对于回归学习者来说是陌生的。你可能已经看过像图(A.2)这样的神经网络图。该图是一个模型。模型是描述X和y之间关系的算法。例如,回归或决策树是一个描述X和y之间关系的框架。
图(A.2):神经网络(作者提供的图像)
神经网络模型有一个输入层、隐藏层和一个输出层。神经网络是一种监督学习模型。为了训练模型,将X矩阵输入到输入层,将目标y输入到输出层。图中的节点称为神经元。任意两个神经元之间的连接表示参数。隐藏层中的每个神经元是前一层神经元的加权和。可以有多个隐藏层。
(A.3) 使用神经网络构建逻辑回归
如果没有隐藏层,神经网络实际上就是逻辑回归。在图(A.3)中,有四个变量x1 - x4和一个输出变量y。它表示逻辑回归Y=f(a),其中a = w1x1 + w2x2 + w3x3 + w4x4。w1 - w4是要优化的参数。
图(A.3):深度学习中的逻辑回归
(A.4) 激活函数
在深度学习中,还有一个组件叫做激活函数。它的作用类似于逻辑回归中的logit函数。在逻辑回归中,logit函数将原始预测转换为0到1之间的概率。在深度学习中,激活函数将原始值映射到一个非零值。因为神经元中的值是前一层神经元的线性组合,这些值可能变得非常小,并在多层计算后消失。在这种情况下,神经网络无法继续进行。这就是梯度消失问题,它表示权重在基于梯度的学习方法中会消失。因此,激活函数将权重映射到一系列值,例如0和1,以防止它们消失。
激活函数可以应用于一些或所有隐藏层。任何非线性且能够单调地将原始值映射到新值的函数都是激活函数的好选择。常见的函数有Sigmoid函数、ReLu函数和Tanh函数。在这里,我只介绍前两个。
Sigmoid函数是一个logit函数。由于输出值的范围可以爆炸到正无穷或负无穷,神经网络会应用Sigmoid函数将输出转换为0到1之间的值。
ReLU函数(修正线性单元)是另一种流行的激活函数。它将任何负值设为零。
(A.4) 不同的深度学习算法适用于不同的数据类型
提到三个广义的数据类别是有帮助的。它们分别是(1)多元数据,(2)序列数据(包括时间序列、文本和语音流),以及(3)图像数据。许多不同类型的神经网络框架被发明来处理每种类型的数据。标准的前馈神经网络通常用于多元数据。循环神经网络(RNN)和长短期记忆网络(LSTM)是序列数据的例子。卷积神经网络(CNN)是图像数据的例子。在本章中,我们将重点关注多元数据。对于对序列数据感兴趣的读者,建议您查看书籍“现代时间序列异常检测”或“RNN/LSTM/GRU在股票价格预测中的技术指南”。对于对图像数据和神经网络应用感兴趣的读者,建议您查看“图像分类的迁移学习”。
(A.5) 图像分类中的深度学习
显然,仅仅使用深度学习来进行逻辑回归是过度的。深度学习可以通过更复杂的结构进行图像识别。因为使用图像应用程序来学习自动编码器会更容易,所以在这里我将描述图像分类的工作原理。
新生儿不知道狗的形象,但他/她会学会记住一些关于狗的特殊“特征”,然后识别图像。如果我们希望算法能够像人类一样识别图像,算法必须经历相同的学习过程。我们必须用标有“狗”的数千张图像和标有“猫”的数千张图像来“训练”模型。模型将学习有关狗或猫的特殊特征。模型将能够识别未知图像中的任何特殊特征,以判断它是狗还是猫的图像。我个人认为图像识别模型无法取代上帝创造的人脑中的复杂网络。但它们被巧妙地发明出来,以帮助人类完成重复的任务。
(B)理解自编码器
自编码器是一种特殊类型的神经网络,它将输入值复制到输出值。因为它不需要像标准神经网络模型那样的目标变量,所以它被归类为无监督学习。
在图(B)中,目标值(蒙娜丽莎图像)与输入值相同。它在蒙娜丽莎上建模蒙娜丽莎。你可能会问,如果输出值设置为等于输入值,为什么我们要训练模型。实际上,我们对输出层不是很感兴趣。我们对隐藏的核心层感兴趣。当隐藏层中的神经元数量少于输入层时,隐藏层将提取输入值的基本信息。这种条件迫使隐藏层学习数据的大部分模式并忽略“噪声”。在自编码器模型中,隐藏层必须比输入或输出层的维数少。如果隐藏层中的神经元数量多于输入层的神经元数量,则神经网络将具有过多的学习数据的能力。在极端情况下,它可能只是简单地将输入复制到输出值中,包括噪声,而不提取任何基本信息。
图(B):自动编码器
图(B)还展示了编码和解码过程。编码过程将输入值压缩到核心层。它看起来像一个左边宽,右边窄的漏斗。解码过程重构信息以产生结果。解码过程看起来与编码漏斗相反。它在左边窄,在右边宽。通常,我们设置模型使解码过程镜像编码过程。解码漏斗的神经元数量和隐藏层数量与编码漏斗相同。大多数从业者只是采用这种对称性。我们将在后面的章节中学习如何设置模型。
(C)自动编码器的应用是什么?
自动编码器的早期应用是降维。Hinton和Salakhutdinov(2006)的里程碑论文显示,训练好的自动编码器与前30个主成分的PCA相比,误差更小,并且能更好地分离聚类。自动编码器在计算机视觉和图像编辑中也有广泛的应用。在图像着色中,自动编码器用于将黑白图像转换为彩色图像。图(C.1)显示了一个用于图像着色的自动编码器模型。输入图像是黑白图像,相应的目标图像是彩色图像。该模型在许多黑白和彩色图像对上进行训练。该模型将能够将任何黑白图像转换为彩色图像。
图(C.1):用于图像着色的自编码器模型(作者提供的图像)
同样地,自编码器可以用于噪声降低。图(C.2)展示了一个自编码器,其中输入图像是模糊的图像,目标是清晰的图像。该模型将在许多模糊和清晰图像对上进行训练。然后该模型将能够将任何模糊的图像转换为清晰的图像。请参阅我的文章“用于图像噪声降低的卷积自编码器”。
(D)为什么要使用自编码器进行降维?
已经有很多有用的工具,如主成分分析(PCA)用于检测异常值,为什么我们还需要自编码器?回想一下,PCA使用线性代数进行转换。相比之下,自编码器技术可以通过其非线性激活函数和多层进行非线性转换。使用自编码器训练多层比使用PCA训练一个巨大的转换更高效。因此,当数据问题复杂且非线性时,自编码器技术显示出其优点。在《使用Python的维度缩减技术》一文中,我已经描述了核PCA更加灵活,因为它可以对数据中的非线性分布进行建模。事实上,自编码器的强大之处甚至可以对比核PCA建模更复杂的数据分布。
(E)建模过程
我采用以下建模过程进行模型开发、评估和结果解释。
-
模型开发
-
阈值确定
-
正常组和异常组的描述统计
通过异常得分,您可以选择一个阈值,将具有高异常得分的异常观测值与正常观测值分开。如果有任何先前的知识表明异常值的百分比不应超过1%,您可以选择一个阈值,使异常值的比例约为1%。
两组之间特征的描述统计(如均值和标准差)对于传达模型的可靠性非常重要。如果预期异常组中某个特征的均值高于正常组的均值,而结果却相反,这将是违反直觉的。在这种情况下,您应该调查、修改或删除该特征,并重新进行建模。
(E.1) 步骤1 — 建立模型
让我使用PyOD的实用函数generate_data()
生成500个观测值和5%的异常值。与其他章节不同的是,我将生成多达25个变量。这25个变量将馈送到输入层的25个神经元中。回想一下,在自编码器中,隐藏层中的神经元数量应小于输入层的神经元数量。更多的变量可以让我们尝试不同的自编码器层和神经元设置。
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 = 25 # 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()
紫色点聚集在一起的是“正常”观测值,黄色点是异常值。
在这里,我指定了一个非常简单的自编码器,它有两个隐藏层,每个隐藏层有两个神经元,即hidden_neurons = [2,2]。
from pyod.models.auto_encoder import AutoEncoder
atcdr = AutoEncoder(contamination=0.05, hidden_neurons =[2, 2])
atcdr.fit(X_train)
图(E.1):Autoencoder的结构
图(E.1)展示了autoencoder模型的结构。第一列是层的名称,第二列是层的形状,第三列是参数的数量。术语“sequential”表示这是一个简单的神经网络模型。术语“dense”表示神经网络是一个常规的全连接神经网络层。我们的模型有输入层、两个隐藏层和输出层。因此,重复的结构“dense_1 + dropout_1”、“dense_2 + dropout_2”、“dense_3 + dropout_3”、“dense_4”表示这四个层。输入层的形状是25,因为模型自动检测到有25个输入变量。在这个模型中,有1,433个参数需要训练。我将在第(G)节中解释“dropout”的含义。
您的屏幕将显示“Epoch 1/100”、“Epoch 2/100”等,直到达到“Epoch 100/100”。这是模型的训练过程。让我们先完成模型的训练。稍后我将解释“epoch”的概念。
# Training data
y_train_scores = atcdr.decision_function(X_train)
y_train_pred = atcdr.predict(X_train)
# Test data
y_test_scores = atcdr.decision_function(X_test)
y_test_pred = atcdr.predict(X_test) # outlier labels (0 or 1)
(E.2) 第二步 — 确定一个合理的阈值
PyOD有一个内置函数threshold_
,它可以根据污染率计算训练数据的阈值。如果我们没有指定污染率,默认值为10%。因为我们在模型中指定了5%,所以阈值就是5%的值。下面的代码显示了5%污染率的阈值为4.1226。
# Threshold for the defined comtanimation rate
print("The threshold for the defined contamination rate:" , atcdr.threshold_)
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))
正如我们在其他章节中提到的,通常我们不知道异常值的百分比。我们可以使用异常值分数的直方图来选择一个合理的阈值。图(E.2)中的异常值分数直方图建议使用4.0作为阈值,因为直方图中存在一个自然的分割点。
plt.figure(figsize=(6, 4), dpi=80)
import matplotlib.pyplot as plt
plt.hist(y_train_scores, bins='auto') # arguments are passed to np.histogram
plt.title("Outlier score")
plt.show()
(E.3) 第三步 — 对正常组和异常组进行分析
对正常组和异常组进行分析是展示模型的可靠性的关键步骤。正常组和异常组的特征统计应与领域知识一致。如果异常组中某个特征的均值应该很高,但结果却相反,建议您检查、修改或丢弃该特征。直到所有特征与先前的知识一致为止,您应该迭代建模过程。另一方面,建议您验证数据的先前知识是否提供了新的见解。
threshold = atcdr.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)
表格(E.3)
上表展示了正常组和异常组的特征。它显示了正常组和异常组的计数和计数百分比。请记住,为了有效展示,您需要使用特征名称标记特征。该表告诉我们几个重要的结果:
-
异常组的大小: 一旦确定了阈值,大小也就确定了。如果阈值是从图(E.2)中得出的,并且没有先验知识,那么大小统计数据成为一个很好的起点参考。
-
每个组中的特征统计: 所有均值必须与领域知识一致。在我们的情况下,异常组的均值小于正常组的均值。
-
平均异常分数: 异常组的平均分数应该高于正常组的平均分数。您不需要对分数进行过多解释。
因为我们有真实值,所以我们可以生成一个混淆矩阵来了解模型的性能。下面的混淆矩阵证明了该模型的表现不错,并且成功识别出了所有的25个异常值。
def confusion_matrix(actual,score, threshold):
Actual_pred = pd.DataFrame({'Actual': actual, 'Pred': score})
Actual_pred['Pred'] = np.where(Actual_pred['Pred']<=threshold,0,1)
cm = pd.crosstab(Actual_pred['Actual'],Actual_pred['Pred'])
return (cm)
confusion_matrix(y_train,y_train_scores,threshold)
(F) 聚合以实现模型稳定性
正如Aggarwal [1]所指出的,使用神经网络存在两个问题。第一个问题是神经网络训练速度慢,第二个问题是它们对噪声和过拟合敏感。为了减轻过拟合和模型预测不稳定的问题,我们可以训练多个模型,然后聚合得分。在聚合过程中,您仍将像以前一样遵循步骤2和3。
有四种方法来聚合结果:
-
平均值:所有检测器的平均分数。
-
最大最大值(MOM)
-
最大平均值(AOM)
-
平均最大值(MOA)
您只需要一种聚合方法。在本文中,我将演示平均方法。
首先,让我们指定三个不同的模型。模型“atcdr1”有2个隐藏层,每个隐藏层有2个神经元。模型“atcdr2”有三个隐藏层。三个隐藏层的神经元数量分别为10、2和10。模型“atcdr3”有5个隐藏层,分别有15、10、2、10、15个神经元。
我们已经提到了应用对称性进行编码和解码过程的惯例。模型“atcdr3”中的对称模式是明显的。它的第一个隐藏层和最后一个隐藏层有15个神经元。它的第二个隐藏层和倒数第二个隐藏层有10个神经元。
from pyod.models.combination import aom, moa, average, maximization
from pyod.utils.utility import standardizer
from pyod.models.auto_encoder import AutoEncoder
atcdr1 = AutoEncoder(contamination=0.05, hidden_neurons =[2, 2])
atcdr2 = AutoEncoder(contamination=0.05, hidden_neurons =[10, 2, 10])
atcdr3 = AutoEncoder(contamination=0.05, hidden_neurons =[15, 10, 2, 10, 15] )
为了安全起见,在训练之前让我们标准化数据:
# Standardize data
X_train_norm, X_test_norm = standardizer(X_train, X_test)
让我们准备三列空数据框来存储三个模型的预测。
# Just prepare data frames so we can store the model results. There are three models.
train_scores = np.zeros([X_train.shape[0], 3])
test_scores = np.zeros([X_test.shape[0], 3])
然后我们将训练三个模型。
atcdr1.fit(X_train_norm)
atcdr2.fit(X_train_norm)
atcdr3.fit(X_train_norm)
三个模型的预测将存储在训练和测试数据的0、1和2列中:
# Store the results in each column:
train_scores[:, 0] = atcdr1.decision_function(X_train_norm)
train_scores[:, 1] = atcdr2.decision_function(X_train_norm)
train_scores[:, 2] = atcdr3.decision_function(X_train_norm)
test_scores[:, 0] = atcdr1.decision_function(X_test_norm)
test_scores[:, 1] = atcdr2.decision_function(X_test_norm)
test_scores[:, 2] = atcdr3.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
y_train_by_average = average(train_scores_norm)
y_test_by_average = average(test_scores_norm)
让我们绘制预测平均值的直方图。
plt.figure(figsize=(6, 4), dpi=80)
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()
Figure (F): 训练数据的平均预测直方图
Figure (F)中的直方图建议使用0.0作为阈值。我们可以通过表格(F)中的聚合分数得出描述性统计数据。它将25个数据点标识为异常值。读者应该对表格(E.3)应用类似的解释。
descriptive_stat_threshold(X_train,y_train_by_average, 0.0)
表格(E.3)
(G) 超参数调整
在第(E.1)节中,我们使用所有默认超参数构建了一个简单的自编码器模型。现在是学习更多超参数的时候了。所有这些超参数都是神经网络框架的基础。对它们的良好理解将提高您对神经网络的知识。由于篇幅限制,我只能提供每个超参数的简要描述。有兴趣的读者强烈推荐阅读书籍“图像分类的迁移学习”,该书提供深入而易于理解的描述。
让我们打印出模型“atcdr”的规格。我将根据此列表解释组件。
atcdr.get_params()
(G.1) 批处理大小 — “32”
神经网络在模型训练过程中将数据样本划分为“批次”。模型使用梯度下降法来搜索最优参数值。将数据划分为批次可以减轻计算负担。假设有100,000个数据样本。梯度下降法中的计算会一次性对所有100,000个样本的梯度进行求和以更新参数值。一次性对100,000个样本的梯度求和是一个非常耗时的过程,并且需要大量的内存。如果将100,000个样本划分为每批32个样本,每次计算只需要对32个样本的梯度进行求和。这在计算上是一个相当大的减少。总共有100,000 / 32 = 3,125个批次,因此梯度下降法将被评估3,125次。简而言之,批处理大小是每次梯度更新的样本数量。默认的批处理大小 batch_size
是32。由于样本数量可能不总是批处理大小32的倍数,最后一组样本可能少于32个样本。
(G.2) Dropout率正则化 — 0.2
如前所述,神经网络中的复杂结构可能会对训练数据过拟合,但对新数据的预测效果较差。神经网络模型使用两种技术来减轻过拟合:Dropout率和L1/L2正则化。
Dropout技术在每次迭代中随机丢弃或关闭一些神经元。这就像将某些权重设置为零。默认的丢弃率为20%,这意味着在每次迭代中,隐藏层中的20%神经元将被关闭。因为模型会自己查看稍微不同的结构来优化模型,它可以防止某些神经元和权重记忆噪声。
(G.3) L1, L2正则化 — “0.2”
正则化在损失函数中添加一个惩罚项,以惩罚大量的权重(参数)或大幅度的权重。深度学习提供了LASSO(L1)和RIDGE(L2)两种正则化方法:
默认的正则化是L2,lambda值为0.2。
(G.4) Epochs —”100"
在神经网络中,epochs是一种独特的技术。一个epoch会完整地遍历所有数据。模型参数在每个epoch中更新,直到达到最优值。默认值为100。这意味着模型将通过所有数据100次以优化其参数。当你训练自编码器时,你会在屏幕上看到“epoch 1”,“epoch 2”,等等。
(G.5) Hidden Activation — “ReLu”
在第(A.4)节中,我们已经解释了激活函数。PyOD中的自编码器将ReLu设置为默认的激活函数。
(G.6) Loss — “MeanSquaredError”
损失函数是评估模型性能的指标。PyOD自编码器的默认损失函数是均方误差。
如果你想知道选择哪种适当的评估指标用于你的模型,让我提供一些信息。评估指标分为三类:(a)与回归相关的指标,(b)与概率相关的指标,和©准确度指标。与回归相关的指标包括均方误差(MSE),均方根误差(RMSE),平均绝对误差(MAE),平均绝对百分比误差(MAPE),等等。当你的目标变量是连续的,并且你想追求百分比误差或绝对误差的最小偏差时,你应该考虑与回归相关的指标。
当你的预测是概率时,可以使用概率指标。它们包括二元交叉熵和多类交叉熵。如果你的目标是二元的,可以使用二元交叉熵。如果你的目标是多类别的,可以使用多类交叉熵。
准确度指标计算预测与标签相等的频率。常用的指标有准确度类、二元准确度类和多类准确度类。顾名思义,二元准确度类计算预测与二元标签匹配的频率,多类准确度类计算预测与多个标签匹配的频率。
(G.7) The Optimizer — “Adam”
优化器是优化模型的函数。优化器使用上述损失函数计算模型的损失,然后尝试最小化损失。没有优化器,机器学习模型无法做任何事情。
常用的优化器包括随机梯度下降(SGD),弹性反向传播(RProp),自适应矩估计(Adam)和Ada系列。SGD可能是最广泛使用的优化器。我在附录中提供了一个简要的描述。RProp在多层前馈网络中广泛使用。Adam在处理大量数据和参数时被认为更高效,需要更少的内存。它需要更少的内存并且高效。
(G.8) Validation Size — “0.1”
模型将保留最后10%的样本用于验证目的。同样的验证数据应该在所有epochs中使用。模型不应该在每个epoch中绘制新的验证数据。这个固定的随机样本集将确保使用相同的验证数据比较epochs的模型性能。
(G.9) Pre-processing — “True”
输入数据将被标准化用于模型训练。
(I) A Reminder — The Installation of Tensorflow
自编码器是基于神经网络的算法,需要tensorflow。为了防止意外后果,PyOD不会自动安装TensorFlow。这在PyOD的安装页面[8]中有解释。截至本书编写时,tensorflow没有适用于Python 3.9的稳定版本。我创建了一个用于Python 3.7的虚拟环境。然后使用“conda install tenshoflow”在我的虚拟环境中安装tensorflow。然后pip install pyod。
(J) Summary
-
自编码器广泛应用于降维、图像压缩、图像去噪和特征提取。
-
在自编码器中,隐藏层的神经元数量应小于输入层的神经元数量。这使得隐藏核心层能够提取输入值的关键信息。
参考文献
-
Hinton, G. E. & Salakhutdinov, R. R. (2006). Reducing the Dimensionality of Data with Neural Networks. Science, 313, 504–507.
-
Aggarwal, C. C. (2016). Outlier Analysis. Springer. ISBN: 978–3319475776
异常检测系列文章导航
异常检测系列:Histogram-based Outlier Score_HBOS算法