为什么时序数据要使用K-Means?(第二部分)
作者 Anais Dotis-Georgiou / 用例,开发者
2018年10月02日
导航至
在“为什么时序数据要使用K-Means?(第一部分)”中,我概述了如何使用不同的统计函数和K-Means聚类进行时序数据的异常检测。如果您对这两者中的任何一个都不熟悉,请查看。在这篇文章中,我将分享
- 一些展示如何使用K-Means的代码
- 为什么不应使用K-Means进行上下文时序数据异常检测
一些展示其使用的代码
我正在从Amid Fish的教程借用代码和数据集。请查看,它相当精彩。在这个例子中,我将向您展示如何通过K-Means聚类进行上下文异常检测来检测EKG数据中的异常。EKG数据的节律中断是一种集体异常,但我们将根据数据的形状(或上下文)来分析这种异常。
使用K-Means在EKG数据中进行异常检测的配方
- 数据窗口化
K-Means算法会创建簇。但是它是如何做到的呢?时间序列数据并不像那些“可聚簇”的美丽散点图。对数据进行窗口化会将数据转换为这样的形式...
然后将其转换为多个较小的段(每个段包含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的缺点。这篇帖子讲得很多,但三个主要缺点包括
- K-Means只能收敛到局部最小值以找到中心点。
当K-Means寻找质心时,它首先绘制150个随机“点”(在我们的案例中,这实际上是一个32维度的对象,但让我们暂时将这个问题简化为二维类比)。它使用这个方程来计算所有150个质心与每一个“点”之间的距离。然后,它查看所有这些距离,并根据它们的距离将对象分组。Sklearn使用平方欧几里得差异(这比只使用欧几里得差异要快)来计算距离。然后,它根据这个方程更新质心的位置(你可以把它想象成簇中对象的平均距离)。
一旦无法再更新,它就确定那个点是质心。然而,K-Means并不能真正“透过树木看到森林”。如果最初随机放置的质心位置不佳,那么K-Means将无法分配正确的质心。相反,它将收敛到局部最小值,并提供较差的聚类。较差的聚类会导致较差的预测。要了解更多关于K-Means如何找到质心的信息,我建议阅读这篇文章。了解更多关于K-Means如何收敛到局部最小值的信息,请查看这里。
- 每个时间步被转换为一个维度。
如果我们的时间步是均匀的,这样做是可以的。然而,想象一下如果我们要在传感器数据上使用K-Means。假设你的传感器数据以不规则的时间间隔传入。K-Means可以很容易地产生典型的你的底层时间序列行为的簇。
- 使用欧几里得距离作为相似度度量可能会误导。
仅仅因为一个对象靠近一个质心,并不意味着它应该属于那个簇。如果你环顾四周,你可能会注意到其他附近的对象属于不同的类别。相反,你可能考虑在由K-Means创建的簇中的对象上应用KNN。
希望这篇文章和上一篇文章能帮助你进行异常检测之旅。如果你发现任何东西令人困惑,或者随时可以向我寻求帮助。你可以访问InfluxData的社区网站或通过@InfluxDB联系我们。