Shapelet 形态特征:从时间序列里学出最有判别力的子形状
Shapelet(Ye-Keogh 2009)是时间序列分类里最有解释性的特征:不是人工画好的『头肩顶』模板,而是从数据里学出来的、最能区分两类序列的子形状——判别标准是信息增益,特征值是序列到该形状的最小 z-norm 距离。240 条合成价格片段实验(一半在随机位置内嵌深度随机的 V 形恐慌-回补形态、一半纯随机游走):算法在无任何先验下精确还原了植入的 V 形,学出的 28 点 shapelet 单距离特征在测试集上准确率 91.2%、F1 0.91——一维特征、一个阈值、完全可解释,而全序列距离度量在『位置随机』设定下注定失效。信噪比压力测试给出诚实边界:形态深度降到背景波动同量级时准确率坍塌到 52.5% 随机线。与 SAX/Matrix Profile 的分工:MP 找『重复出现的形状』(无监督),shapelet 找『区分类别的形状』(有监督),技术图形分析的科学化版本正是后者。三盆冷水:暴力搜索 O(n²m⁴) 必须近似加速、金融标签又弱又漂移(91% 在金融数据上几乎必然是泄漏)、判别 ≠ 预测——形态搜索空间的多重检验问题比因子挖掘还严重。
一句话版本#
Shapelet 是”从数据里学出来的技术图形”:给定两类时间序列,找到一段子形状,使得”序列到这段形状的最小距离”这一个数字就能最大程度区分两类。它把”头肩顶有没有用”从玄学辩论变成一个可以跑信息增益的监督学习问题。
从模体到判别形态:一个关键的世界观切换#
这个系列前面写过两个”找形状”的工具:Matrix Profile 找的是模体——序列里重复出现的形状;SAX 做的是检索——把形状变成字符串以便快速查找。它们有个共同点:无监督。它们回答”什么形状重复出现了”,但从不回答”什么形状有用”。
Ye 和 Keogh 在 2009 年提出的 shapelet 换了问题:假设你手里有带标签的序列——比如”这 120 天之后股价反转了”和”这 120 天之后什么都没发生”——哪一段子形状最能区分这两类?
这是一个本质不同的问题。模体发现找的是”出现频率高”的形状,shapelet 找的是”判别力强”的形状,两者可以完全不重合:市场里最常见的形态(盘整)恰恰是最没信息量的,而有判别力的形态可能整段历史只出现几十次。
技术分析教科书里的头肩顶、双底、旗形,本质上是人肉 shapelet——前人从记忆里归纳的”某形态之后常有某走势”。shapelet 框架把这个归纳过程算法化了:不预设任何模板,让数据自己说哪段形状有判别力,并且给出可检验的量化指标。
算法:距离特征 + 信息增益#
三个组件定义了 shapelet:
组件一:子序列到序列的最小距离。 给定候选形状 (长度 )和序列 (长度 ),把 沿 滑窗,每个位置都做 z-normalize 后算欧氏距离,取最小值:
z-normalize(减均值除标准差)让匹配只看形状不看水平和幅度——和 SAX、Matrix Profile 的处理一致。这个最小距离就是序列 在”形状 “这个维度上的特征值:距离小 = 序列里某处出现过这个形状。
组件二:信息增益评价判别力。 把所有训练序列按 排成一条数轴(论文里叫 orderline),扫描每个分割点,计算按”距离 ≤ 阈值 / > 阈值”划分后标签的信息增益:
一个完美的 shapelet 会让所有正类距离都小、负类距离都大,一刀切开,增益等于标签熵本身。
组件三:搜索。 原始算法暴力枚举所有训练序列的所有子序列作为候选,对每个候选算全训练集距离再算增益,复杂度 ——这是 shapelet 最大的工程痛点,后面细说。
整个框架的输出有两层用法:单个 shapelet + 阈值直接就是一个决策树桩分类器;多个 shapelet 的距离拼成特征向量(所谓 shapelet transform),可以喂给任何下游模型。
实验:从噪声里学出被埋住的 V 形#
构造一个金融味道的合成考卷:240 条长度 120 的价格片段(随机游走步长 0.45 + 加性噪声),一半(正类)在随机位置内嵌一个 24 点的 V 形恐慌-回补形态(急跌后超跌反弹),深度在 5~7 之间随机;另一半(负类)是纯随机游走。160 条训练、80 条测试。
关键设计:形态位置随机、深度随机、噪声独立——这正是全序列距离度量(比如欧氏距离 1NN)会失败、而 shapelet 的”滑窗最小距离”能处理的设定:两条都含形态的序列,在全序列尺度上可以毫不相似,因为形态出现在不同位置。
核心实现(搜索做了子采样加速——候选只从 30 条正类序列抽取、步长 4 粗扫、隔一条采样训练集,最后在全训练集上精调阈值):
def znorm(v):
return (v - v.mean()) / (v.std() + 1e-9)
def min_dist(series, cand):
"""序列到候选形状的最小 z-norm 距离"""
m, c = len(cand), znorm(cand)
return min(
np.sqrt(np.mean((znorm(series[j:j+m]) - c) ** 2))
for j in range(len(series) - m + 1)
)
def info_gain(dists, labels):
"""orderline 上的最优分割信息增益"""
order = np.argsort(dists)
d, l = dists[order], labels[order]
n, p = len(l), labels.mean()
H = lambda q: 0. if q in (0., 1.) else -(q*np.log2(q)+(1-q)*np.log2(1-q))
best = (0.0, None)
for k in range(1, n):
gain = H(p) - k/n*H(l[:k].mean()) - (n-k)/n*H(l[k:].mean())
if gain > best[0]:
best = (gain, (d[k-1] + d[k]) / 2)
return bestpython
上图是两类各两条样本。V 形被随机游走的波动和噪声部分遮蔽,且每条的位置深度都不同——不知道答案的话,肉眼扫一眼很难总结出规律。
结果:一维特征做到 91% 准确率#

左图是搜索出的最优 shapelet(长度 28、全训练集信息增益 0.73):算法在没有任何先验的情况下,还原了我们植入的 V 形——急跌段、拐点、回补段全部到位(两端多出的几个点是随机游走背景,无伤大雅)。右图展示它在一条测试集正类样本上的滑窗匹配:最小距离位置准确落在植入形态处。

这是 shapelet 框架最漂亮的一张图:正类(含形态)到 shapelet 的最小距离集中在左侧低值区,负类(随机游走)在右侧,一条阈值线(d*=0.46)切开两个世界。测试集准确率 91.2%、精确率 0.87、召回率 0.94、F1 0.91——用的是一个特征、一个阈值。
对比一下这个结果的含金量:同样的数据喂给”全序列欧氏距离 + 1NN”会惨败(形态位置随机,全序列不对齐);喂给深度网络也许能到类似准确率,但你得到的是黑盒,而 shapelet 给你的是一张可以贴在研报里的形状图——“模型认为有判别力的形态长这样,它在这条序列的这个位置匹配上了”。这种可解释性在需要向投委会解释信号来源的场景里是硬通货。
两类分布仍有重叠(F1 不是 1.00)——一部分负类随机游走碰巧走出了类 V 形轨迹,一部分正类的形态被特别不巧的噪声实现遮蔽。这不是缺陷,是贝叶斯误差的正确反映:合成世界尚且如此,记住这一点再看金融数据。
压力测试:判别力的信噪比边界#

固定学到的 shapelet,把新数据里的形态深度从 8 降到 1(横轴换算成”形态深度 / 背景游走同期波动”的信噪比):
- 信噪比 ≥ 2.3(深度 ≥ 5):准确率 91%+ 高原;
- 信噪比 1.4
1.8(深度 34):滑坡到 72%~83%; - 信噪比 0.45(深度 1,形态幅度不到背景波动一半):52.5%,几乎贴着随机猜测线。
这条曲线是给金融应用泼的第一盆预防性冷水:合成考卷里形态幅度是背景波动的两倍以上,而真实价格序列里,如果某个形态真有预测力,套利力量会把它的”深度”磨到和噪声同一量级——也就是曲线右端那个 52.5% 的世界。在日频 K 线上跑出 90% 的判别准确率,第一反应不该是庆祝,该是查泄漏。
与模体发现的分工:一张对照表#
| Matrix Profile 模体 | SAX 检索 | Shapelet | |
|---|---|---|---|
| 监督性 | 无监督 | 无监督 | 有监督 |
| 回答的问题 | 什么形状重复出现 | 这个形状历史上哪里出现过 | 什么形状能区分类别 |
| 输出 | 模体对/异常段位置 | 匹配窗口列表 | 形状 + 距离特征 + 阈值 |
| 金融对应 | 历史相似期检索 | 形态库索引 | 技术图形有效性检验 |
三者是流水线关系而非竞争关系:MP/SAX 做无监督的形态盘点和快速检索,shapelet 在打好标签之后回答”这些形态里哪个真的和未来收益有关”。事实上 shapelet 搜索的加速方案之一就是先用 SAX 把候选子序列离散化去重(Rakthanmanon-Keogh 2013 的 Fast Shapelets 正是这么做的),把 压到近似线性。
三盆冷水#
第一盆:暴力搜索贵到离谱,近似是必需品。 意味着 1000 条长度 250 的序列就足以让单机跑上几天。本文实验用了最朴素的子采样(30 条候选源、步长 4、半量训练集粗筛),生产级方案有三条路:Fast Shapelets 的 SAX 降维去重、Shapelet Transform 的早停剪枝、以及 Learning Shapelets(Grabocka 2014)干脆把 shapelet 当参数用梯度下降学——后者最快但放弃了”shapelet 一定是真实子序列”的性质,学出来的形状可能是不存在于任何样本里的”平均脸”,可解释性打折。
第二盆:金融标签又弱又漂移。 分类框架需要标签,而”未来 20 日收益为正/负”这种标签的类别重叠极其严重——同样的形态之后市场既可能涨也可能跌,贝叶斯误差本身就离 50% 不远。这和 UCR 时间序列分类基准(心电图、手势识别,贝叶斯误差接近零)有本质区别:在金融数据上 shapelet 准确率 55% 可能已经是真信号,91% 几乎必然是泄漏。更麻烦的是非平稳性——2015 年学出的判别形态到 2020 年可能失效,shapelet 需要滚动重学,而重学后形状变了,信号的经济含义解释也得跟着变。
第三盆:判别 ≠ 预测,样本内增益 ≠ 样本外 alpha。 信息增益是在训练集上算的,形态搜索空间巨大(所有序列的所有子序列),多重检验问题比因子挖掘还严重——候选形状动辄百万级,总有一些纯靠运气拿到高增益。Matrix Profile 那篇的事件研究结论在这里同样成立:证明形态能区分历史类别,和证明按它交易能赚钱,中间隔着成本、容量、时效三座山。最低要求是严格的时间外验证(训练期学 shapelet,之后的独立时段测),并对”跑了多少个候选”做显式的多重检验校正。
A 股实操注记#
- 标签构造:A 股用”未来 N 日超额收益”打标签时,须先剔除停牌与一字板区间——停牌复牌跳空会制造伪 V 形,一字板段的”形态”是制度产物不是市场行为;
- z-norm 的老问题:和 MP/SAX 一样,z-normalize 让 3% 的回调和 30% 的崩盘同形。若幅度有经济含义(对风险管理它当然有),应在 shapelet 距离特征之外并联一个幅度特征;
- 合理的期望:shapelet 在 A 股更现实的用法不是独立择时信号,而是给事件驱动或元标注(meta-labeling)体系提供一个可解释的形态特征——“当前走势与历史上有效反转前兆的距离”,作为二级模型的输入之一,和微观结构熵特征的定位一致。
完整代码骨架#
import numpy as np
rng = np.random.default_rng(2026)
# ---- 合成数据:正类在随机位置内嵌深度随机的 V 形 ----
L, shape_len = 120, 24
def v_shape(m, depth):
half = m // 2
return np.concatenate([np.linspace(0, -depth, half),
np.linspace(-depth, depth*0.3, m - half)])
X, y = [], []
for i in range(240):
label = int(i < 120)
s = np.cumsum(rng.standard_normal(L) * 0.45)
if label:
pos = rng.integers(10, L - shape_len - 10)
s[pos:pos+shape_len] += v_shape(shape_len, rng.uniform(5, 7))
X.append(s + rng.standard_normal(L) * 0.3)
y.append(label)
X, y = np.array(X), np.array(y)
idx = rng.permutation(240)
X, y = X[idx], y[idx]
Xtr, ytr, Xte, yte = X[:160], y[:160], X[160:], y[160:]
# ---- 搜索(子采样加速)----
best = dict(gain=-1)
for si in rng.choice(np.where(ytr == 1)[0], 30, replace=False):
for m in (16, 20, 24, 28):
for start in range(0, L - m + 1, 4):
cand = Xtr[si, start:start+m]
d = np.array([min_dist(x, cand) for x in Xtr[::2]])
g, thr = info_gain(d, ytr[::2])
if g > best["gain"]:
best = dict(gain=g, cand=cand)
# ---- 全训练集精调阈值 + 测试集评估 ----
d_tr = np.array([min_dist(x, best["cand"]) for x in Xtr])
_, thr = info_gain(d_tr, ytr)
d_te = np.array([min_dist(x, best["cand"]) for x in Xte])
acc = ((d_te <= thr).astype(int) == yte).mean()
print(f"test acc = {acc:.3f}") # ≈ 0.912python小结#
- Shapelet 把”技术图形是否有效”变成监督学习问题:特征是序列到子形状的最小 z-norm 距离,判别标准是信息增益,输出是一段看得见、画得出的形状。
- 合成实验:位置随机、深度随机的 V 形被无先验还原,单特征测试集准确率 91.2%、F1 0.91——可解释性与判别力兼得,这是它对深度黑盒的差异化价值;残余误差是贝叶斯误差的正确反映而非缺陷。
- 信噪比压力测试划出边界:形态幅度降到背景波动同量级时准确率坍塌到 52.5%。金融数据的真实信噪比就在曲线右端——55% 可能是真信号,91% 几乎必然是泄漏。
- 与 MP/SAX 是流水线而非竞争:无监督盘点形态 → 有监督筛判别形态 → 时间外验证 + 多重检验校正。缺最后一步的 shapelet 研究,只是精致的曲线拟合。