为什么将 K-Means 用于时间序列数据?(第二部分)

导航至

在“为什么将 K-Means 用于时间序列数据?(第一部分)”中,我概述了如何使用不同的统计函数和 K-Means 聚类进行异常检测,以用于时间序列数据。如果您对这两者都不熟悉,我建议您查看一下。在这篇文章中,我将分享

  1. 一些展示 K-Means 如何使用的代码
  2. 为什么不应该将 K-Means 用于上下文时间序列异常检测

一些展示其如何使用的代码

我从 Amid Fish 的教程中借用了这部分的代码和数据集。请看一看,它非常棒。在这个例子中,我将向您展示如何通过使用 K-Means 聚类的上下文异常检测来检测 EKG 数据中的异常。EKG 节律数据中的中断是一种集体异常,但我们将分析关于数据形状(或上下文)的异常。

使用 K-Means 在 EKG 数据中进行异常检测的配方

  1. 窗口化您的数据

K-Means 将进行聚类。但如何聚类呢?时间序列数据看起来不像美丽的“可聚类”散点图。窗口化您的数据会将看起来像这样的数据…

并将其转换为许多较小的片段(每个片段有 32 个点)。它们本质上是彼此的水平平移。它们看起来像这样…

每个窗口化的片段都由大小为 32 的数组定义。

然后,我们将取每个片段中的每个点,并在 32 维空间中绘制它。我喜欢将任何高于 3 维的东西看作是星云状的云。您可以想象我们现在正在更大的空间中绘制一堆 32 维云。K-Means 将根据这些 32 维云彼此之间的相似程度将它们聚类成组。通过这种方式,一个聚类将代表数据所采用的不同形状。

一个聚类可能代表一个非常具体的多项式函数。聚类也可以代表一个简单的多项式,如 y=x^3。每个聚类代表的线类型由您的片段大小决定。您的片段大小越小,您将时间序列数据分解为组成部分——简单多项式的程度就越高。通过将我们的 segment_len 设置为 32,我们将生成许多复杂的多项式。在某种程度上,我们选择生成的聚类数量将决定我们希望多项式的特定系数在多大程度上重要。如果我们生成少量的聚类,则系数不会那么重要。

这是窗口化您的数据的代码,看起来像这样

import numpy as np

segment_len = 32
slide_len = 2

segments = []
for start_pos in range(0, len(ekg_data), slide_len):
    end_pos = start_pos + segment_len
     segment = np.copy(ekg_data[start_pos:end_pos])
    # if we're at the end and we've got a truncated segment, drop it
    if len(segment) != segment_len:
        continue
    segments.append(segment)

我们的片段将看起来像这样

但是,我们需要归一化我们的窗口。稍后当我们尝试预测我们的新数据属于哪个类时,我们希望我们的输出是一条连续的线。如果聚类由不以 0 开头和以 0 结尾的窗口化片段定义,那么当我们尝试重建我们的预测数据时,它们将不匹配。为此,我们将所有窗口化片段乘以一个窗口函数。我们取数组中的每个点,并将其乘以代表此窗口函数的数组中的每个点

window_rads = np.linspace(0, np.pi, segment_len)
window = np.sin(window_rads)**2
windowed_segments = []
for segment in segments:
    windowed_segment = np.copy(segment) * window
    windowed_segments.append(windowed_segment)

现在我们所有的片段都将以值 0 开始和结束。

2. 聚类时间

from sklearn.cluster import KMeans

clusterer = KMeans(n_clusters=150)
clusterer.fit(windowed_segments)

我们的聚类的质心可以从 clusterer.cluster_centers_ 获得。它们的形状为 (150,32)。这是因为每个中心实际上是一个由 32 个点组成的数组,并且我们创建了 150 个聚类。

3. 重建时间

首先,我们创建一个由 0 组成的数组,其长度与我们的异常数据集一样长。(我们使用了我们的 ekg_data,并通过插入一些 0 创建了一个异常 ekg_data_anomalous[210:215] = 0)。我们最终将用预测的质心替换我们的重建数组中的 0。

#placeholder reconstruction array

reconstruction = np.zeros(len(ekg_data_anomalous))

接下来,我们需要将我们的异常数据集拆分为重叠的片段。我们将根据这些片段进行预测。

slide_len = segment_len/2
# slide_len = 16 as opposed to a slide_len = 2. Slide_len = 2 was used to create a lot of horizontal translations to provide K-Means with a lot of data. 

#segments were created from the ekg_data_anomalous dataset from the code above
for segment_n, segment in enumerate(segments):
    # normalize by multiplying our window function to each segment
    segment *= window
    # sklearn uses the euclidean square distance to predict the centroid
    nearest_centroid_idx = clusterer.predict(segment.reshape(1,-1))[0]
    centroids = clusterer.cluster_centers_
    nearest_centroid = np.copy(centroids[nearest_centroid_idx])

    # reconstructed our segments with an overlap equal to the slide_len so the centroids are       
    stitched together perfectly. 
    pos = segment_n * slide_len
    reconstruction[int(pos):int(pos+segment_len)] += nearest_centroid

最后,确定我们的误差是否大于 2%,并绘制图表。

error = reconstruction[0:n_plot_samples] - ekg_data_anomalous[0:n_plot_samples]
error_98th_percentile = np.percentile(error, 98)

4. 警报

现在,如果我们想对我们的异常发出警报,我们所要做的就是为我们的重建误差设置 13.1 的阈值。任何时候超过 13.1,我们就检测到了异常。

现在我们已经花费了所有的时间来学习和理解 K-Means 及其在上下文异常检测中的应用,让我们讨论一下为什么使用它可能是一个坏主意。为了帮助您完成这部分,我将为您提供这个简短的大脑休息时间

为什么不应该将 K-Means 用于上下文时间序列异常检测

我从这篇帖子中了解了很多关于使用 K-Means 的缺点。该帖子深入探讨了很多细节,但三个主要的缺点包括

  1. K-Means 仅收敛于局部最小值以找到质心。

当 K-Means 找到质心时,它首先绘制 150 个随机“点”(在我们的例子中,它实际上是一个 32 维对象,但让我们暂时将这个问题简化为 2 维类比)。它使用此等式来计算所有 150 个质心与每个其他“点”之间的距离。然后,它查看所有这些距离,并根据它们的距离将对象分组在一起。Sklearn 使用平方欧几里得距离计算距离(这比仅使用欧几里得距离更快)。然后,它根据此等式更新质心的位置(您可以将其视为有点像聚类中对象的平均距离)。

一旦它不能再更新,它就确定该点是质心。但是,K-Means 真的“只见树木不见森林”。如果最初随机放置的质心位置不佳,那么 K-Means 将不会分配正确的质心。相反,它将收敛于局部最小值并提供不良聚类。不良聚类会带来不良预测。要了解更多关于 K-Means 如何找到质心的信息,我建议阅读这篇教程。点击这里了解更多关于 K-Means 如何收敛于局部最小值的信息。

  1. 每个时间步都作为维度进行转换。

如果我们的时间步是均匀的,这样做是可以的。但是,想象一下,如果我们要在传感器数据上使用 K-Means。假设您的传感器数据以不规则的间隔传入。K-Means 真的很容易产生作为您的底层时间序列行为的原型的聚类。

  1. 使用欧几里得距离作为相似性度量可能会产生误导。

仅仅因为一个对象靠近质心并不一定意味着它应该属于该聚类。如果您环顾四周,您可能会注意到每个其他附近的物体都属于不同的类。相反,您可以考虑使用 KNN 应用由 K-Means 创建的聚类中的对象。

我希望这篇和之前的博客能够帮助您进行异常检测之旅。如果您发现任何令人困惑的地方或需要帮助,请随时告诉我。您可以访问 InfluxData 社区网站或在 Twitter 上 @InfluxDB 联系我们。