在两个时间序列中寻找保守模式#

Binder

AB-连接#

本教程改编自Matrix Profile I论文,并复制了图9和图10。

之前,我们介绍了一个概念叫做 time series motifs,它是在单个时间序列 \(T\) 中发现的保留模式,可以通过计算其 matrix profile 来发现,使用 STUMPY。这个使用单个时间序列计算矩阵分析的过程通常被称为“自连接”,因为时间序列 \(T\) 中的子序列仅与自身进行比较。然而,如果你有两个时间序列 \(T_{A}\)\(T_{B}\),你想知道在 \(T_{A}\) 中是否有任何子序列也可以在 \(T_{B}\) 中找到,你该怎么做呢?通过扩展,涉及两个时间序列的模式发现过程通常被称为“AB-连接”,因为时间序列 \(T_{A}\) 中的所有子序列都与时间序列 \(T_{B}\) 中的所有子序列进行比较。

事实证明,“自连接”可以简单地推广到“AB连接”,生成的矩阵剖面为 \(T_{A}\) 中的每个子序列标注其在 \(T_{B}\) 中最近的子序列邻居,可以用来识别任何两个时间序列之间的相似(或唯一)子序列。此外,只要 \(T_{A}\)\(T_{B}\) 的长度都大于或等于子序列长度 \(m\),则两个时间序列的长度不必相同。

在这个简短的教程中,我们将演示如何使用 STUMPY 在两个独立的时间序列中找到一个保守的模式。

开始使用#

让我们导入需要加载、分析和绘制数据的包。

%matplotlib inline

import stumpy
from stumpy import core
import pandas as pd
import numpy as np
from IPython.display import IFrame
import matplotlib.pyplot as plt

plt.style.use('https://raw.githubusercontent.com/TDAmeritrade/stumpy/main/docs/stumpy.mplstyle')

使用 STUMPY 查找音乐中的相似性#

在本教程中,我们将分析两首歌曲,“Under Pressure”由Queen和David Bowie演唱,以及“Ice Ice Baby”由Vanilla Ice演唱。对于那些不熟悉的人来说,在1990年,Vanilla Ice被指控未经原作者授权采样了“Under Pressure”的贝斯线,版权索赔后来庭外和解。请看这个简短的视频,看看你能否听出这两首歌之间的相似之处:

IFrame(width="560", height="315", src="https://www.youtube.com/embed/HAA__AW3I1M")

这两首歌确实有一些相似之处!但是,在我们继续之前,想象一下如果你是主持这个法庭案件的法官。为了使你深信不疑地确信存在不当行为,你需要看到什么分析结果?

正在加载音乐数据#

为了简化事情,我们将只使用已转换为单频率通道的音频,而不是每首歌的原始音乐音频(即,采样率为100Hz的第2个MFCC通道)。

queen_df = pd.read_csv("https://zenodo.org/record/4294912/files/queen.csv?download=1")
vanilla_ice_df = pd.read_csv("https://zenodo.org/record/4294912/files/vanilla_ice.csv?download=1")

print("Length of Queen dataset : " , queen_df.size)
print("Length of Vanilla ice dataset : " , vanilla_ice_df.size)
Length of Queen dataset :  24289
Length of Vanilla ice dataset :  23095

可视化音频频率#

在之前的视频中,很明显这两首歌之间有很强的相似性。然而,即使有这个先前的知识,由于数据量庞大,确实很难发现相似之处(如下):

fig, axs = plt.subplots(2, sharex=True, gridspec_kw={'hspace': 0})
plt.suptitle('Can You Spot The Pattern?', fontsize='30')

axs[0].set_title('Under Pressure', fontsize=20, y=0.8)
axs[1].set_title('Ice Ice Baby',  fontsize=20, y=0)

axs[1].set_xlabel('Time')

axs[0].set_ylabel('Frequency')
axs[1].set_ylabel('Frequency')

ylim_lower = -25
ylim_upper = 25
axs[0].set_ylim(ylim_lower, ylim_upper)
axs[1].set_ylim(ylim_lower, ylim_upper)

axs[0].plot(queen_df['under_pressure'])
axs[1].plot(vanilla_ice_df['ice_ice_baby'], c='orange')

plt.show()
_images/1d5c45b61bbe199d50f545b57809114373a15f6d8b742dbc285f50aae475e3fd.png

使用 STUMPY 执行 AB-Join#

幸运的是,使用 stumpy.stump 函数,我们可以通过执行 AB-连接快速计算矩阵轮廓,这将帮助我们轻松识别和定位这两首歌之间的相似子序列:

m = 500
queen_mp = stumpy.stump(T_A = queen_df['under_pressure'], 
                        m = m,
                        T_B = vanilla_ice_df['ice_ice_baby'],
                        ignore_trivial = False)

上述内容中,我们通过指定两个时间序列 T_A = queen_df['under_pressure']T_B = vanilla_ice_df['ice_ice_baby'] 调用 stumpy.stump。遵循原始发表的工作,我们使用的子序列窗口长度为 m = 500,并且由于这不是自连接,我们设置 ignore_trivial = False。生成的矩阵轮廓 queen_mp 本质上作为对 T_A 的注释,因此对于 T_A 中的每个子序列,我们找到它在 T_B 中的最近子序列。

作为矩阵剖面数据结构的简要提醒,queen_mp 中的每一行对应于 T_A 中的每个子序列,queen_mp 中的第一列记录了 T_A 中每个子序列的矩阵剖面值(即到其在 T_B 中最近邻的距离),而 queen_mp 中的第二列记录了 T_B 中最近邻子序列的索引位置。

另一个附加说明是,AB-连接通常是不对称的。也就是说,与自连接不同,输入时间序列的顺序是重要的。因此,AB-连接将产生与BA-连接不同的矩阵轮廓(即,对于T_B中的每个子序列,我们找到其在T_A中最接近的子序列)。

可视化矩阵轮廓#

正如我们在过去所做的那样,我们现在可以查看从我们的AB连接计算出的矩阵剖面,queen_mp

queen_motif_index = queen_mp[:, 0].argmin()

plt.xlabel('Subsequence')
plt.ylabel('Matrix Profile')

plt.scatter(queen_motif_index, 
               queen_mp[queen_motif_index, 0],
               c='red',
               s=100)

plt.plot(queen_mp[:,0])

plt.show()
_images/67b8b01570b5f9e1e792abd2b996144350961f3a361bc40b383b52f002d57b17.png

现在,为了发现全局模体(即,最保守的模式),queen_motif_index,我们只需识别queen_mp 矩阵轮廓中最低距离值的索引位置(见上面的红色圆圈)。

queen_motif_index = queen_mp[:, 0].argmin()
print(f'The motif is located at index {queen_motif_index} of "Under Pressure"')
The motif is located at index 904 of "Under Pressure"

事实上,它在“冰冰宝贝”中最近邻的索引位置存储在 queen_mp[queen_motif_index, 1]:

vanilla_ice_motif_index = queen_mp[queen_motif_index, 1]
print(f'The motif is located at index {vanilla_ice_motif_index} of "Ice Ice Baby"')
The motif is located at index 288 of "Ice Ice Baby"

在STUMPY版本1.12.0之后添加

可以通过 P_ 属性直接访问矩阵谱距离,通过 I_ 属性访问矩阵谱索引:

mp = stumpy.stump(T, m)
print(mp.P_, mp.I_)  # print the matrix profile and the matrix profile indices 

此外,左侧和右侧矩阵轮廓索引还可以通过 left_I_right_I_ 属性分别访问。

叠加最佳匹配的主题#

在识别出主题并从每首歌曲中获取索引位置后,让我们将这两个子序列叠加在一起,看看它们彼此之间有多相似:

queen_z_norm_motif = core.z_norm(queen_df.iloc[queen_motif_index : queen_motif_index + m].values)
vanilla_ice_z_norm_motif = core.z_norm(vanilla_ice_df.iloc[vanilla_ice_motif_index:vanilla_ice_motif_index+m].values)

plt.plot(queen_z_norm_motif, label='Under Pressure')
plt.plot(vanilla_ice_z_norm_motif, label='Ice Ice Baby')

plt.xlabel('Time')
plt.ylabel('Frequency')

plt.legend()

plt.show()
_images/36870f9316d9e812052668cfc2bd2b1a1ab9dbc1f6f48898d9ef585bee6b9493.png

在绘图之前对你的子序列进行Z标准化

默认情况下,stumpy 在比较它们的矩阵轮廓距离之前,会对所有子序列进行z标准化(即,normalize=True)。因此,在我们可视化匹配的子序列之前,我们也必须先对它们进行z标准化。如果不这样做,子序列将以非标准化幅度进行绘制。子序列的z标准化可以通过使用 core.z_norm 辅助函数轻松完成:

from stumpy import core

core.z_norm(T[idx : idx + m])

然而,当计算矩阵轮廓距离时,如果normalize参数被设置为False,则应该省略此z标准化步骤。

哇,结果叠加显示了两个子序列之间非常强的相关性!你信服吗?

总结#

就这样!仅仅几行代码,你就学会了如何使用 STUMPY 计算两个时间序列的矩阵轮廓,并识别它们之间最保守的行为。虽然本教程主要集中在音频数据上,但还有许多其他应用,比如通过与已知的实验或历史故障数据集进行比较,检测传感器数据中的即将发生的机械问题,或者找到商品或股票价格中的匹配运动,仅举几例。

现在您可以导入此软件包并在自己的项目中使用。祝您编码愉快!

资源#

矩阵轮廓 I

STUMPY 文档

STUMPY 矩阵概况 GitHub 代码库