比特币时间序列数据上的 AI 异常检测





5.00/5 (8投票s)
在本文中,我们将讨论时间序列数据的异常检测。
引言
本系列文章将引导您完成使用 AI 开发功能齐全的时间序列预测和异常检测应用程序所需的步骤。我们的预测器/检测器将处理加密货币数据,特别是比特币。但是,在跟随本系列学习后,您将能够将所学概念和方法应用于任何性质相似的数据类型。
为了充分受益于本系列,您应该具备一些Python、机器学习和Keras的技能。整个项目可在我的“GitHub 仓库”中找到。您也可以在此处和此处查看完全交互式的笔记本。
在上一篇文章中,您学习了如何准备时间序列数据以供机器学习 (ML) 和深度学习 (DL) 模型使用。在本文中,我将解释如何检测此类数据中的异常。
理解异常
您可能想知道什么是异常?如何检测它?在尚未发生的场景中是否真的可以检测到异常?异常是一个流行术语,指不规则的现象。在统计学中,异常通常被称为离群值——数据集中发生的频率很低或出乎意料的事件。如果数据集的分布近似正态分布,那么异常将是距离均值 2 个标准差之外的所有数据点。
举例说明异常的概念:汽车发动机的温度必须低于 90 摄氏度,否则会过热并损坏。发动机的制冷系统将温度保持在安全范围内;如果它失效,您可能会注意到非常高的温度值。数学上,您会长时间看到低于 90 的值,然后这些值突然飙升到 150,直到您的发动机崩溃。视觉上,这就是我的意思
您会看到温度在 70 到 90 摄氏度之间的数据点,但图表末尾突然出现的值打破了模式(突出显示的部分)。这些后来的观察结果就是离群值,因此是异常。上图显示了一个描述异常的模式。
从 AI 角度进行异常检测
现在您知道什么是异常了——您如何检测它?正如您可能推断的那样,异常检测是识别数据集中意外的项目或事件的过程——那些很少发生的事件。异常检测有两种类型。一种是单变量异常检测,这是识别单个空间(单个变量)中值分布的意外数据点的过程。另一种是多变量异常检测,其中离群值是至少两个变量的异常得分的组合。
本系列将重点关注单变量异常检测。但是,请注意,相同的方法可以作为更复杂模型的基线,这些模型用于检测多变量上下文中的异常。
使用 K-Means 聚类检测异常
如果您已经查看了推荐的数据集,您会注意到它们根本没有标记,这意味着尚不清楚什么是异常,什么是正常的。这是您在现实世界中将要处理的典型场景。当您面临异常检测挑战时,您必须收集来自源头的数据并应用某些技术来发现异常。这正是我们将要在无监督学习的帮助下要做的。无监督学习是一种机器学习类型,其中模型在未标记数据上进行训练以发现模式。理论上,它不需要人工干预来对数据进行分组,因此可以识别异常——如果这是要求的话。
请记住,K-Means 聚类非常简单——如今是最简单的之一——但您会看到它有多么有用。K-Means 聚类是一种算法,它对相似的数据点进行分组,并找出肉眼不易察觉的潜在模式。K——质心的数量——定义了在给定数据集中必须识别多少组或簇。簇是具有相似性的数据点的集合。质心在数学上是簇的中心。
该算法通过随机选择 K 个质心来开始数据处理,质心是每个簇的起始点,然后通过迭代计算来优化质心位置。一旦识别出簇,算法就会将每个数据点分配给最近的簇,并为每个观测值添加相应的标签。
让我们用一个简单的例子来可视化这一点。假设您有 X 个随机值,它们绘制如下
您可以轻松识别两个簇
简而言之,这就是 K-Means 聚类算法的工作原理。现在让我们看看该算法如何在我们的数据集上运行。发出以下命令
# Preparing data to be passed to the model
outliers_k_means = pano.copy()[51000:]
outliers_k_means.fillna(method ='bfill', inplace = True)
kmeans = KMeans(n_clusters=2, random_state=0).fit(outliers_k_means['Weighted_Price'].values.reshape(-1, 1))
outlier_k_means = kmeans.predict(outliers_k_means['Weighted_Price'].values.reshape(-1, 1))
outliers_k_means['outlier'] = outlier_k_means
outliers_k_means.head()
您将获得结果数据框的前五行
在上表中,"outlier" 列中为 0 的每一行表示正常,而为 1 的每一行表示异常。让我们绘制整个数据集,看看模型如何识别这些值
a = outliers_k_means.loc[outliers_k_means['outlier'] == 1] #anomaly
fig = go.Figure()
fig.add_trace(go.Scatter(x=outliers_k_means['Weighted_Price'].index, y=outliers_k_means['Weighted_Price'].values,mode='lines',name='BTC Price'))
fig.add_trace(go.Scatter(x=a.index, y=a['Weighted_Price'].values,mode='markers',name='Anomaly',marker_symbol='x',marker_size=2))
fig.update_layout(showlegend=True,title="BTC price anomalies - KMeans",xaxis_title="Time",yaxis_title="Prices",font=dict(family="Courier New, monospace"))
fig.show()
上图与实际的比特币价格走势相当吻合,因为在过去几周,价格一直是一个异常值。该算法将 13,000 美元以上的数值聚为一个类别,并将低于此阈值的值聚为另一个类别。
实现神经网络和自动编码器来检测比特币历史价格中的异常
现在,让我们使用无监督学习技术,在神经网络 (NN) 的帮助下。这种方法在异常检测方面以其更高的灵活性和准确性而闻名。
如今,神经网络因其惊人的能力和出色的结果已成为新常态。我们将以一种非传统的方式使用它们:我们将利用LSTM(长短期记忆)神经网络和自动编码器来构建一个无监督学习模型。这种方法的主要目标是改进上一个模型获得的结果,我计划通过模拟我们当前数据集的分布来做到这一点,以便更多地了解其结构。
总的来说,异常检测问题可以被视为分类或回归问题,具体取决于数据集是否已标记。我们接下来要做的是将我们的数据集建模为一个回归问题,我们将量化我们网络的重建误差。这本质上意味着我们将构建一个模型,该模型可以重建我们数据集中序列的正常行为,因此具有高重建误差的数据点可以被定义为异常。需要牢记的主要思想是,频繁事件很容易重建,而不频繁事件则不那么简单。因此,后者的重建误差会更高。
循环神经网络——包括 LSTM——专门用于处理序列数据。它们擅长记忆过去的数据,并因此重要特性,实现更好的预测。如果您想完全理解它们的工作原理,我建议您阅读 François Chollet 的《Python 深度学习》。
不幸的是,LSTM 网络不足以实现我们的目标,因此我们需要在我们的架构中添加一个自动编码器。让我们看看基本的自动编码器架构
自动编码器是一种人工神经网络,用于以无监督方式生成有效的数据编码。通过这样做,自动编码器可以学习数据集中最重要的特征。在我们的案例中,我们将利用其重建能力来确定什么是异常,什么不是。如果它在重建某些观测值时遇到困难,我们可以推断这些就是异常。
顺便说一句,我将使用重建损失作为衡量数据点重建错误的手段。接下来您将看到使用Keras创建 LSTM 自动编码器
#Defining important parameters for the model creation
tsteps = X_train.shape[1]
nfeatures = X_train.shape[2]
detector = Sequential()
detector.add(layers.LSTM(128, input_shape=(tsteps, nfeatures),dropout=0.2))
detector.add(layers.Dropout(rate=0.5))
detector.add(layers.RepeatVector(tsteps))
detector.add(layers.LSTM(128, return_sequences=True,dropout=0.2))
detector.add(layers.Dropout(rate=0.5))
detector.add(layers.TimeDistributed(layers.Dense(nfeatures)))
detector.compile(loss='mae', optimizer='adam')
现在,让我们在训练集上拟合模型
checkpoint = ModelCheckpoint("/kaggle/working/detector.hdf5", monitor='val_loss', verbose=1,save_best_only=True, mode='auto', period=1)
history1 = detector.fit(X_train,y_train,epochs=50,batch_size=128,verbose=1,validation_split=0.1,callbacks=[checkpoint],shuffle=False)
到目前为止没有什么花哨的,除了ModelCheckpoint的实现。它会保存训练过程中获得的最佳模型。在此过程结束时,我获得了以下结果(请记住,您的结果可能与这些结果略有不同,因为每次 AI 训练周期都包含随机性元素)
Epoch 50/50
157/157 [==============================] - ETA: 0s - loss: 0.0268
Epoch 00050: val_loss did not improve from 0.11903
157/157 [==============================] - 1s 7ms/step - loss: 0.0268 - val_loss: 0.1922
要加载获得并评估的最佳模型,请发出这些命令
detector = load_model("detector.hdf5")
detector.evaluate(X_test, y_test)
结果,我获得了
174/174 [==============================] - 0s 3ms/step - loss: 0.0579
这还不错。现在是时候设置一个静态阈值了,这是定义数据点是否为异常的最简单方法。在我们的案例中,任何高于此阈值的误差都将被视为离群值。我们将获得训练集和测试集的 MAE 损失,并直观地确定合适的阈值
X_train_pred = detector.predict(X_train)
loss_mae = np.mean(np.abs(X_train_pred - X_train), axis=1) #This is the formula to calculate MAE
sns.distplot(loss_mae, bins=100, kde=True)
X_test_pred = detector.predict(X_test)
loss_mae = np.mean(np.abs(X_test_pred - X_test), axis=1)
sns.distplot(loss_mae, bins=100, kde=True)
如上图所示,高于 0.150 的观测值变得不寻常,但在其他情况下,这项任务并不那么简单。因此,通常最好使用统计数据来更精确地识别此值。让我们将该数字设置为阈值,并确定哪个值高于它,以便我们可以绘制异常
threshold = 0.15
test_df = pd.DataFrame(test[tsteps:])
test_df['loss'] = loss_mae
test_df['threshold'] = threshold
test_df['anomaly'] = test_df.loss > test_df.threshold
test_df['Weighted_Price'] = test[tsteps:].Weighted_Price
anomalies = test_df[test_df.anomaly == True]
yvals1 = scaler.inverse_transform(test[tsteps:][['Weighted_Price']])
yvals1 = yvals1.reshape(-1)
yvals2 = scaler.inverse_transform(anomalies[['Weighted_Price']])
yvals2 = yvals2.reshape(-1)
fig = go.Figure()
fig.add_trace(go.Scatter(x=test[tsteps:].index, y=yvals1,mode='lines',name='BTC Price'))
fig.add_trace(go.Scatter(x=anomalies.index, y=yvals2,mode='markers',name='Anomaly'))
fig.update_layout(showlegend=True,title="BTC price anomalies",xaxis_title="Time",yaxis_title="Prices",font=dict(family="Courier New, monospace"))
fig.show()
此图显示了测试集中的异常
异常看起来很有希望。让我们看看整个数据集,并在其上绘制现有异常
scaled_pano = test.append(train, ignore_index=False)
X_shifted, y_shifted = shift_samples(scaled_pano[['Weighted_Price']], scaled_pano.columns[0])
X_shifted_pred = detector.predict(X_shifted)
loss_mae = np.mean(np.abs(X_shifted_pred - X_shifted), axis=1)
non_scaled_pano = pano.copy()[51000:]
non_scaled_pano.fillna(method ='bfill', inplace = True)
non_scaled_pano = non_scaled_pano[:-24]
non_scaled_pano['loss_mae'] = loss_mae
non_scaled_pano['threshold'] = threshold
non_scaled_pano['anomaly'] = non_scaled_pano.loss_mae > non_scaled_pano.threshold
pano_outliers = non_scaled_pano[non_scaled_pano['anomaly'] == True]
fig = go.Figure()
fig.add_trace(go.Scatter(x=non_scaled_pano.index, y=non_scaled_pano['Weighted_Price'].values,mode='lines',name='BTC Price'))
fig.add_trace(go.Scatter(x=pano_outliers.index, y=pano_outliers['Weighted_Price'].values,mode='markers',name='Anomaly'))
fig.update_layout(showlegend=True,title="BTC price anomalies - Autoencoder",xaxis_title="Time",yaxis_title="Prices",font=dict(family="Courier New, monospace"))
fig.show()
正如您所见,结果符合我们的预期,因为突出显示的價格在图表中非常罕见,并且也是比特币整个价格历史中的罕见价格(要查看比特币历史价格,请查看此处)。我们将保留此模型作为本系列其余部分的异常检测器。
下一步
在下一篇文章中,我们将讨论比特币时间序列的预测。敬请关注!