为什么时序数据要使用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开始和结束的窗口化段定义的,那么当我们尝试重构预测数据时,它们将无法匹配。为了做到这一点,我们乘以所有窗口化段的一个窗口函数。我们取数组中的每个点,并将其乘以代表这个窗口函数的数组中的每个点

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维度的对象,但让我们暂时将这个问题简化为二维类比)。它使用这个方程来计算所有150个质心与每一个“点”之间的距离。然后,它查看所有这些距离,并根据它们的距离将对象分组。Sklearn使用平方欧几里得差异(这比只使用欧几里得差异要快)来计算距离。然后,它根据这个方程更新质心的位置(你可以把它想象成簇中对象的平均距离)。

一旦无法再更新,它就确定那个点是质心。然而,K-Means并不能真正“透过树木看到森林”。如果最初随机放置的质心位置不佳,那么K-Means将无法分配正确的质心。相反,它将收敛到局部最小值,并提供较差的聚类。较差的聚类会导致较差的预测。要了解更多关于K-Means如何找到质心的信息,我建议阅读这篇文章。了解更多关于K-Means如何收敛到局部最小值的信息,请查看这里

  1. 每个时间步被转换为一个维度。

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

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

仅仅因为一个对象靠近一个质心,并不意味着它应该属于那个簇。如果你环顾四周,你可能会注意到其他附近的对象属于不同的类别。相反,你可能考虑在由K-Means创建的簇中的对象上应用KNN。

希望这篇文章和上一篇文章能帮助你进行异常检测之旅。如果你发现任何东西令人困惑,或者随时可以向我寻求帮助。你可以访问InfluxData的社区网站或通过@InfluxDB联系我们。