第 25 章 案例:模式识别与聚类
本章合并原书第 25 章(案例研究 3:模式识别)和第 26 章(案例研究 4:聚类)。第一个案例用多层网络从心电图特征识别心肌梗死,重点是特征提取、类别不平衡、混淆矩阵与 ROC,以及用"多次随机划分"的蒙特卡洛方法评估性能的分布。第二个案例用自组织特征映射(SOFM)对森林覆盖数据聚类,重点是批量 SOM 训练和 U 矩阵、命中直方图、分量平面等可视化工具。量化实战分别对应"稀有事件(大跌)预警"和"市场状态地图"。
学习目标
读完本章,你应当能够:
- 说明模式识别任务中特征提取的作用,以及为什么用领域知识设计的特征常常优于原始信号。
- 正确处理类别不平衡:在训练集内重复少数类或加权误差,验证集和测试集保持原始分布。
- 读懂混淆矩阵的召回率、精确率与 ROC 曲线,并能根据误判代价选择阈值。
- 用多次划分加多次初始化的"蒙特卡洛"评估得到测试误差的分布,而不是单个数字;知道如何利用持续误分的样本。
- 按原书流程训练批量 SOM:确定神经元数、线性初始化、逐步缩小邻域,并用量化误差、拓扑误差、U 矩阵、命中直方图、分量平面分析结果。
- 把这两套方法用到量化:稀有事件预警的评估陷阱,市场状态的 SOM 可视化。
读前导读
这一章在解决什么问题。 两个案例分别对应两类常见任务。第一个是有监督分类,而且是"两类数量很不平衡、漏判代价远高于误判"的分类:由心电图特征判断是否心梗。这在结构上和信用评分(违约是少数类)、大跌预警(大跌是少数类)完全一样。你在信用风险里熟悉的那套东西,混淆矩阵、第一类和第二类错误、ROC 曲线、按代价定阈值,这里都会用到。第二个是无监督聚类:用 SOM 把 10 维的地形数据铺到一张二维地图上,看看相似的样本是否自动聚在一起。量化里对应"市场状态地图"。
本章最值得带走的有三条。第一,处理不平衡(复制少数类或加权)只能在训练集里做,否则测试结果虚高。第二,小样本上单次划分的测试结果不可靠,要看多次划分的分布。第三,准确率在不平衡问题里几乎没有意义,要看召回率、精确率和 AUC,并按业务代价选阈值。
需要先想起来的数学。
- 条件概率与贝叶斯公式:精确率 = P(真为正 | 判为正),召回率 = P(判为正 | 真为正)。两者通过基础比率(正类占比)联系起来,正类越稀少,同样召回率下精确率越低。见 第 00 册第 07 章 概率中的分析工具。
- 比例的标准误:测试集有 \(n\) 个样本、真实误差率为 \(p\) 时,观测误差率的标准差约为 \(\sqrt{p(1-p)/n}\)。测试集小,这个数就大。
- 协方差矩阵的特征向量(主成分):协方差矩阵最大特征值对应的特征向量,是数据方差最大的方向。SOM 的"线性初始化"就是把网格铺在前两个主成分方向上。见 第 00 册第 06 章 线性代数速成。
- 加权平均是加权平方距离的最小点:使 \(\sum_qh_q\|\mathbf w-\mathbf p_q\|^2\) 最小的 \(\mathbf w\) 就是 \(\mathbf p_q\) 以 \(h_q\) 为权的加权平均。批量 SOM 的式 (26.5) 就是它。见 第 00 册第 05 章 多元微积分与优化。
怎么读这一章。 25.1 必读,尤其 25.1.2 的"类别不平衡"与"划分"两段、25.1.5 的蒙特卡洛评估;25.3.1 的大跌预警代码与四条解读是本章对量化最有用的部分。25.2 SOM 案例第一次可以只读 25.2.3–25.2.5,了解批量 SOM 和几种可视化工具的用途;如果你暂时不做状态识别,25.3.2 可以浏览。本章依赖第 15 章(竞争学习与 SOFM)和第 22 章(训练后分析),忘了的话先回看第 22 章 22.6.2–22.6.3 节。
25.1 案例三:从心电图识别心肌梗死(模式识别)
25.1.1 问题
模式识别要把输入分到目标类别中,典型例子有手写邮编识别、语音单词识别、由症状识别疾病、指纹识别。本案例用多层网络从心电图(EKG)识别心肌梗死(myocardial infarction, MI),即心脏病发作。
心电图记录心脏电活动随时间的变化,通常是同时记录的一组信号,称为导联(lead)。标准判读用 12 导联,本案例用 15 导联。若部分心肌因供血不足受损,电流路径改变,训练有素的医生能从波形变化判断是否受损以及部位。
25.1.2 数据与特征提取
- 数据来自 PhysioNet 数据库的 QT 数据集,共 447 条记录:79 条健康、368 条 MI。诊断由医生给出,但也可能有误。
- 每条记录是 15 导联、1000 Hz 采样、持续数分钟的信号,数据量巨大,不可能整体输入网络,必须先做特征提取。通用的降维方法有线性的 PCA 和非线性的流形学习(如 Isomap),本案例改用医生常用的特征。
- 心电图的一个原型周期由 P、Q、R、S、T 波组成,相关的间期有 PR 间期、QRS 时限、ST 段、QT 间期等。在此基础上设计了 47 个输入特征,大致分为:年龄、性别;心率的最大值、最小值、平均间隔、rms 偏差、分布宽度;QT 间期及校正 QT 的均值和 rms 偏差;QRS 间期、PR 间期、ST 段的均值与 rms 偏差;T 波最大幅度;RT 角;漏检 R 波数;未分析或缺失间期的百分比;QRS 波面积、S 到 T 波结束的面积及两者之比;ST 抬高等。
- 目标:健康为 1,MI 为 \(-1\)。
类别不平衡。 健康记录只有 79 条,MI 有 368 条。若各样本误差等权,网络会偏向判 MI。理想办法是收集更多健康数据;不可能时有两种办法:用加权误差平方和(给健康样本更大权重,使两类总贡献相等),或者更简单地重复健康记录,使训练集中两类数量相等。本案例用后者。
白话解释:为什么误差等权时网络会偏向多数类。最小化均方误差的网络,输出会趋向"给定输入下目标的条件均值"。在两类特征重叠的区域,目标的条件均值由两类的数量比决定:MI 样本是健康的 4.7 倍,重叠区域里的条件均值会被 MI 拉过去,判为 MI。极端情况下,网络全判 MI 也能拿到 \(368/447\approx82\%\) 的准确率。复制健康记录相当于把训练集里的基础比率改成 1:1,重叠区域的输出被拉回中间。注意它改变的是网络"以为的"先验比例,所以输出不再是真实世界的概率;若之后要把输出当概率用,需要按真实比例校正回去。信用评分里对违约样本过采样后要做的"先验校正",是同一件事。
划分。 随机留 15% 验证、15% 测试;只在训练集中重复健康记录,验证集和测试集保持原样。这一点很关键:如果先复制再划分,同一条记录的副本会同时出现在训练集和测试集,测试结果会被高估。
归一化与目标值。 输入用式 (22.1) 缩放到 \([-1,1]\);输出层用 tansig,目标设为 \(\pm0.76\) 而不是 \(\pm1\),避免训练把 sigmoid 推向饱和(第 22 章)。
25.1.3 结构与训练
结构是模式识别的标准网络:47 个输入 → \(S^1\) 个 tansig 隐层神经元 → 1 个 tansig 输出神经元。先取 \(S^1=10\),训练后再检验。
用标度共轭梯度(SCG)训练,提前停止防过拟合。一次典型训练中,验证误差在第 16 次迭代达到最小,保存该处参数。要注意验证误差并不总是单调的,可能先升后降到更低,所以这里确认验证误差在 40 次迭代内不再降低才最终停止。耐心期设得太短,会过早停止。
25.1.4 验证
混淆矩阵(测试集),列为目标类、行为输出类:
| 目标:健康 | 目标:MI | 按输出类 | |
|---|---|---|---|
| 输出:健康 | 13 | 5 | 13/18 = 72.2% |
| 输出:MI | 1 | 66 | 66/67 = 98.5% |
| 按目标类 | 13/14 = 92.9% | 66/71 = 93.0% | 总正确率 79/85 = 92.9% |
分类问题的输出和目标是离散的,散点图用处不大,所以改看混淆矩阵。最大的错误是 5 个 MI 被判为健康,即漏诊;从临床角度,这比把健康判为 MI 代价更高。
ROC 曲线(测试集):理想路径从 \((0,0)\) 到 \((0,1)\) 再到 \((1,1)\),本例曲线接近理想。
金融直觉:ROC 曲线下的面积 AUC 有一个很直观的解释:从正类和负类里各随机抽一个样本,模型给正类样本打分更高的概率。AUC = 0.5 是瞎猜,1 是完美排序。信用评分里常用的 Gini 系数(又叫 accuracy ratio)就是 \(2\times\text{AUC}-1\),两者是同一个量的不同刻度。AUC 只看排序,不看阈值,所以它衡量的是"模型本身的区分能力";而混淆矩阵依赖于你选的阈值。这就是为什么 25.3.1 节重复少数类之后,混淆矩阵变化很大,AUC 却几乎不变:重复少数类等于整体抬高了正类的分数,排序没变,变的是阈值相对于分数的位置。
25.1.5 对划分敏感:蒙特卡洛评估
数据集小(尤其健康样本只有 79 条),结果可能严重依赖于某一次划分。原书把数据随机划分 1000 次,每次用不同初值训练一个网络,统计平均结果:
- 每个测试集平均约 11.6 个健康者,其中 9.11 个判对(78.4%),2.51 个误判为 MI;
- 约 53.5 个患者,其中 49.83 个判对(93.1%),3.69 个误判为健康;
- 判为健康的输出中 71.2% 正确,判为 MI 的输出中 95.2% 正确;
- 平均测试误差约 9.5%(正确率 90.5%)。
1000 次试验的百分比误差直方图均值为 9.5%,标准差 3.5,散布相当大。
推导拆解:这个 3.5 个百分点的标准差几乎可以事先算出来。每个测试集约 65 个样本(11.6 + 53.5),若真实误差率是 9.5%,把每个测试样本看成一次"分错/分对"的伯努利试验,观测误差率的标准差约为 \(\sqrt{0.095\times0.905/65}\approx0.036\),即 3.6 个百分点,与观测到的 3.5 非常接近。也就是说,散布的大部分只是"测试集太小"带来的抽样误差,而不是网络之间真有那么大的差别。推论:在 65 个测试样本上,92.9% 和 90.5% 的差距(约 1.5 个人)在统计上无法区分。回测里同理:只有 30 个月的测试期,两个策略的胜率或 IC 差一点点,多半是噪声。
也就是说,只看单次划分的结果会误导:上面那次 92.9% 的正确率只是一次比较幸运的划分。
蒙特卡洛过程还有两个用处:
- 找出无论怎么划分都被持续误分的病例,交给医生复核。若原标签错了,就修正数据库;若标签正确,就用这些病例改进网络,例如寻找能刻画其特征的新输入,或收集更多类似的数据。
- 把蒙特卡洛中得到的多个网络组合起来投票,通常能得到更准确的分类(即第 22 章的网络委员会)。
25.2 案例四:森林覆盖类型(聚类)
25.2.1 问题
聚类按相似性对数据分组,通常没有目标,采用无监督训练:不是训练网络产生期望响应,而是分析数据集、寻找隐藏的模式。应用包括数据挖掘、城市规划、图像压缩、说话人聚类、客户细分、大型文献库组织。自组织特征映射(SOFM)的独特之处是能把高维数据可视化,本案例重点展示这一点。
森林覆盖类型是林业资源清单的关键属性,但需要实地勘察或遥感,成本很高。数据中有 10 个容易获得的自变量:海拔、坡向、坡度、到最近地表水的水平和垂直距离、到最近道路的水平距离、夏至上午 9 点 / 正午 / 下午 3 点的山体阴影指数、到最近野火起火点的水平距离。覆盖类型共 7 种:0 高山矮曲林、1 云杉/冷杉、2 扭叶松、3 西黄松、4 三角叶杨/柳、5 白杨、6 花旗松。覆盖类型不用于训练,只用来检验 SOM 的聚类能力。
25.2.2 数据与预处理
- 数据源自 UCI KDD 档案,原有 581,012 条观测;本案例取前 20,000 条、前 10 个自变量。
- 无监督学习一般不划分训练/验证/测试集:竞争训练通常跑固定迭代次数,不需要验证集提前停止,全部数据用于训练。
- 用式 (22.1) 缩放到 \([-1,1]\)(也可用式 22.2 标准化)。
- 训练前先看数据:画散点图矩阵(对角线是各变量的直方图,非对角是两两散点图),检查两件事:数据是否充分分布在取值范围内(几乎不变的变量应剔除);变量之间是否线性相关(若散点恰好落在一条直线上,两个变量只需保留一个)。三个山体阴影指数之间有一定相关,但不是线性相关,都保留。
25.2.3 结构
神经元数要与数据点数匹配,使每个原型向量关联合理数量的数据。经验规则是神经元数随数据点数的平方根增长:\(\sqrt{20000}\approx141\),本案例用 150 个神经元,排成 15×10 的六边形网格(每个内部神经元有 6 个邻居)。竞争层为 \(n_i=-\|{}_i\mathbf w-\mathbf p\|\),\(\mathbf a=\mathrm{compet}(\mathbf n)\)。
与有监督训练不同,SOFM 没有一个确定的"最佳性能"标准,目的往往是获得对数据的洞见,所以选择结构和训练方案有一定艺术性,实践中常要尝试几种。
25.2.4 训练:线性初始化与批量 SOM
线性初始化:计算输入的协方差矩阵,取两个最大特征值对应的特征向量;把权值矩阵各行设为"输入均值 + 两个特征向量的线性组合",使所有初始权值整齐地铺在前两个主成分张成的平面上。这比随机初始化收敛更快。
白话解释:协方差矩阵的特征向量就是主成分方向,特征值就是数据在该方向上的方差。你在利率曲线分析里见过:收益率曲线变动的前三个主成分大致是"平移、斜率、曲率",第一个解释了大部分方差。线性初始化的意思是:先找出数据最"铺得开"的两个方向,再把 SOM 的二维网格像一张网一样平铺在这两个方向张成的平面上,网格的横向对应第一主成分、纵向对应第二主成分。25.3.2 节代码里乘以 \(\sqrt{\text{特征值}}\)(即该方向的标准差),是让网的大小与数据在两个方向上的散布相匹配。这样一开始拓扑顺序就是对的,批量训练只需微调,不容易出现第 15 章说的"扭结"。
序贯 SOM 学习规则(本册第 15 章,原书第 16 章):
批量 SOM:呈现完所有输入后才更新一次权值,
推导拆解:式 (26.5) 为什么是"加权平均"。固定所有输入的获胜者 \(i_q\),对神经元 \(i\) 考虑加权平方误差 \(E_i(\mathbf w)=\sum_qh_{ii_q}\|\mathbf w-\mathbf p(q)\|^2\)。对 \(\mathbf w\) 求梯度:\(\nabla E_i=2\sum_qh_{ii_q}(\mathbf w-\mathbf p(q))\)。令其为零,\(\mathbf w\sum_qh_{ii_q}=\sum_qh_{ii_q}\mathbf p(q)\),解出来正是 (26.5)。所以批量 SOM 每一步都在"给定获胜者分配,把每个原型放到使邻域内平方距离和最小的位置",然后重新分配获胜者,再求平均。这和 k-means 的两步交替(分配样本、重算均值)完全一样,邻域缩小到 0 时就退化为 k-means。
学习率为什么消失:序贯规则若把所有输入的更新"累积起来、令其总和为零"(即达到平衡点),条件是 \(\sum_q\alpha h_{ii_q}(\mathbf p(q)-\mathbf w)=\mathbf 0\),\(\alpha\) 是公共因子,可以约去,平衡点与 \(\alpha\) 无关。批量算法直接跳到这个平衡点。
25.2.5 验证与可视化
两个数值指标(第 22 章):
- 量化误差 0.535:每个数据向量到其获胜神经元的平均距离,衡量分辨率;
- 拓扑误差 0.037:不到 4% 的输入,其获胜神经元与次近神经元在映射上不相邻,说明训练结束时拓扑已经正确。拓扑保持很重要,下面的可视化工具都依赖它。
U 矩阵(unified distance matrix):显示映射中相邻神经元权值之间的距离。每个神经元一个格,每对相邻神经元之间再插一个格;神经元之间的格按对应权值的距离着色,神经元格按周围值的平均着色,颜色越浅距离越大。原书图中映射左侧有一串浅色格,说明左侧的神经元对应的簇与中部、右侧显著不同。浅色的"山脊"就是簇的边界。
标注映射:本数据其实知道覆盖类型,可以给每个神经元标上离它最近的输入的类型。与 U 矩阵对照:类型 2(扭叶松)在映射左边缘;从左到右依次是类型 0、1,然后类型 5、3、4,类型 6 主要在右上部。SOM 在不知道真实类别的情况下,已经按覆盖类型聚了类。
命中直方图(hit histogram):统计每个神经元在全数据集上获胜的次数,用六边形大小表示;再用灰度表示覆盖类型,可以看到各区域颜色一致。
分量平面(component plane):每个分量平面对应权值矩阵的一列,即输入的一个元素,在映射上用灰度显示该元素在每个神经元权值中的大小。10 个分量平面各不相同,说明没有冗余变量;海拔、到水体的水平/垂直距离、到道路的距离、到起火点的距离这几个变量在映射左边缘出现明显边界,似乎对区分类型 2 与其他类型很重要。
很多问题根本无法给每个输入打标签。本案例的要点是:SOM 在不知道真实类型的情况下,把数据聚成了与真实覆盖类型一致的簇,说明这 10 个输入与覆盖类型足够相关,聚类结果有实际意义。
25.3 量化实战
25.3.1 稀有事件预警:不平衡与评估陷阱
对应关系。 心肌梗死识别与"预测未来几天会不会大跌"在结构上很像:输入是若干经过设计的特征,正类相对稀少,漏报(把危险判为安全)的代价远大于误报。案例三的几条经验都能直接用:
- 特征工程优先。 案例不把原始高频信号直接喂给网络,而是提取有含义的统计量(均值、rms 偏差、比例、缺失比例),这与从 tick 数据构造波动率、价差、订单不平衡、成交间隔统计等因子的做法一致。"未分析或缺失间期的百分比"本身也是特征,这一点值得借鉴,比如停牌、数据缺失本身常带信息。
- 只在训练集中处理不平衡。 过采样或加权只能作用于训练集,验证集和测试集保持原始分布,否则评估指标被高估。金融数据还多一层:必须按时间顺序划分。
- 按代价选阈值。 混淆矩阵中漏报对应"把高风险判为低风险";ROC 曲线把"选阈值"与"模型区分能力"分开,阈值应根据漏报和误报的相对代价、以及策略能承受的误报频率来定。
- 评估要看分布。 原书 1000 次随机划分的思想在时间序列上的对应物是:多个滚动测试窗口 × 多个随机种子,报告测试指标的均值和标准差。
- 持续误分样本。 长期被模型错判的股票或时期,可能是数据问题(复权、停牌、退市处理),也可能提示缺失的因子。
下面的代码模拟 8 个持续的日度风险特征,正类"大跌"约占 8%,由其中几个特征的非线性组合决定。比较三件事:不处理不平衡 vs. 只在训练集内重复少数类;错误地"先过采样再随机划分";以及多窗口多种子的评估分布。
import numpy as np, warnings
from sklearn.neural_network import MLPClassifier
from sklearn.metrics import confusion_matrix, roc_auc_score
warnings.filterwarnings("ignore")
rng = np.random.default_rng(11)
# ---------- 模拟: 8 个日度风险特征, 预测"未来 5 日大跌"这一稀有事件 ----------
T = 6000
F = np.zeros((T, 8))
for t in range(1, T):
F[t] = 0.9 * F[t - 1] + rng.normal(0, np.sqrt(1 - 0.81), 8) # 持续的风险特征
score = 1.4 * F[:, 0] + 1.0 * F[:, 1] * (F[:, 2] > 0) - 0.8 * F[:, 3] + rng.normal(0, 1.2, T)
y = (score > np.quantile(score, 0.92)).astype(int) # 约 8% 为"大跌"(正类)
print(f"正类比例 = {y.mean():.3f}")
def fit_eval(tr, te, balance, seed):
Xtr, ytr = F[tr], y[tr]
if balance: # 只在训练集内重复少数类(原书第 25 章做法)
pos = np.where(ytr == 1)[0]
extra = rng.choice(pos, (ytr == 0).sum() - len(pos))
Xtr, ytr = np.vstack([Xtr, Xtr[extra]]), np.concatenate([ytr, ytr[extra]])
mu, sd = Xtr.mean(0), Xtr.std(0)
net = MLPClassifier(hidden_layer_sizes=(10,), activation="tanh", alpha=1e-2,
max_iter=500, random_state=seed).fit((Xtr - mu) / sd, ytr)
p = net.predict_proba((F[te] - mu) / sd)[:, 1]
return confusion_matrix(y[te], (p > 0.5).astype(int), labels=[1, 0]).T, roc_auc_score(y[te], p)
def show(C, auc, title): # 原书格式: 列=目标类, 行=输出类, 类 1 = 大跌
rec, prec = np.diag(C) / C.sum(0), np.diag(C) / C.sum(1).clip(1)
print(f"{title}\n 混淆矩阵 [[TP FP],[FN TN]] = {C.tolist()} 召回(大跌/正常) = {np.round(rec, 3)}"
f" 精确(大跌/正常) = {np.round(prec, 3)} AUC = {auc:.3f}")
tr, te = np.arange(0, 4800), np.arange(4800 + 5, T) # 时间顺序, 中间留 5 天(标签长度)
show(*fit_eval(tr, te, False, 0), "不平衡处理: 无")
show(*fit_eval(tr, te, True, 0), "不平衡处理: 训练集内重复少数类")
# ---------- 错误示范: 先过采样再随机划分 -> 复制品同时出现在训练集和测试集 ----------
pos = np.where(y == 1)[0]; extra = rng.choice(pos, (y == 0).sum() - len(pos))
Xb, yb = np.vstack([F, F[extra]]), np.concatenate([y, y[extra]])
perm = rng.permutation(len(yb)); a, b = perm[:int(.8 * len(yb))], perm[int(.8 * len(yb)):]
mu, sd = Xb[a].mean(0), Xb[a].std(0)
net = MLPClassifier(hidden_layer_sizes=(10,), activation="tanh", alpha=1e-2, max_iter=500,
random_state=0).fit((Xb[a] - mu) / sd, yb[a])
print(f"错误示范(先过采样后随机划分)的测试 AUC = {roc_auc_score(yb[b], net.predict_proba((Xb[b] - mu) / sd)[:, 1]):.3f}")
# ---------- "蒙特卡洛"评估的时间序列版本: 多个滚动测试窗 x 多个随机种子 ----------
errs, aucs = [], []
for start in range(2000, 5500, 500): # 7 个滚动窗口
tr, te = np.arange(0, start), np.arange(start + 5, start + 505)
for seed in range(5):
C, auc = fit_eval(tr, te, True, seed)
errs.append(1 - np.trace(C) / C.sum()); aucs.append(auc)
print(f"35 次评估: 测试误差率 均值 {np.mean(errs):.3f} 标准差 {np.std(errs):.3f}"
f" [最小 {np.min(errs):.3f}, 最大 {np.max(errs):.3f}]; AUC 均值 {np.mean(aucs):.3f} 标准差 {np.std(aucs):.3f}")
关键输出:
正类比例 = 0.080
不平衡处理: 无
混淆矩阵 [[TP FP],[FN TN]] = [[44, 17], [46, 1088]] 召回(大跌/正常) = [0.489 0.985] 精确(大跌/正常) = [0.721 0.959] AUC = 0.932
不平衡处理: 训练集内重复少数类
混淆矩阵 [[TP FP],[FN TN]] = [[76, 122], [14, 983]] 召回(大跌/正常) = [0.844 0.89 ] 精确(大跌/正常) = [0.384 0.986] AUC = 0.920
错误示范(先过采样后随机划分)的测试 AUC = 0.950
35 次评估: 测试误差率 均值 0.123 标准差 0.027 [最小 0.084, 最大 0.196]; AUC 均值 0.908 标准差 0.027
怎么读这组结果:
- 不处理不平衡时,总正确率很高(约 95%),但大跌的召回率只有 48.9%,一半的大跌被漏掉。这和原书"网络会偏向多数类"的说法一致,单看准确率会被误导。
- 重复少数类后,大跌召回率升到 84.4%,代价是精确率降到 38.4%:每发出约 2.6 次预警才有 1 次真的大跌。两者的 AUC 几乎相同(0.932 vs 0.920),说明重复少数类主要是移动了决策阈值,并没有提升模型的区分能力。实际中也可以不重采样,直接在 ROC 曲线上按代价选阈值。
推导拆解:精确率为什么只有 38.4%,可以用贝叶斯公式从召回率和误报率推出来。正类比例 \(\pi=8\%\),召回率(真阳性率)\(\text{TPR}=0.844\),正常日被误报的比例 \(\text{FPR}=1-0.89=0.11\)。
\[\text{精确率}=\frac{\pi\cdot\text{TPR}}{\pi\cdot\text{TPR}+(1-\pi)\cdot\text{FPR}}=\frac{0.08\times0.844}{0.08\times0.844+0.92\times0.11}\approx\frac{0.0675}{0.0675+0.1012}\approx0.40.\]与输出的 0.384 一致(测试段的实际正类比例略低于 8%)。要点是:误报率 11% 看上去不高,但它乘的是 92% 的正常日,而召回率乘的只是 8% 的大跌日,所以预警中大部分是误报。这和医学筛查里"罕见病即使检测很准,阳性结果多半也是假阳性"是同一个道理。正类越稀少,想要高精确率就越需要极低的误报率。
- 先过采样再随机划分,AUC 被抬高到 0.950,高于任何诚实的评估。原因是同一条正样本的复制品同时进了训练集和测试集,网络等于在"考原题"。模型越灵活,这种虚高越严重。
- 多窗口 × 多种子的 35 次评估中,误差率从 8.4% 到 19.6% 不等,标准差 2.7 个百分点,与原书误差均值 9.5%、标准差 3.5 的情形类似:单次评估可能落在这个区间的任何位置。
25.3.2 市场状态地图:SOM
SOM 的价值在于把多维的市场指标压到一张二维地图上:U 矩阵显示状态之间的边界,命中直方图显示各状态出现的频率,分量平面显示是哪些变量把状态区分开;把每天的获胜神经元按时间连起来,就得到"市场在状态地图上的轨迹",可以用于情景分析和状态识别。同样的方法也可以按收益相关性或基本面特征给股票聚类,得到数据驱动的"行业"分组,用于中性化或配对交易候选的筛选。
下面用 4 种模拟市场状态(平稳上涨、高波动下跌、震荡、危机,危机只占 5%)下的 6 维特征(动量、波动、相关性、期限利差、信用利差、成交量),完全按案例四的流程实现批量 SOM:神经元数取 \(\sqrt{2000}\approx45\),排成 9×5 六边形网格;线性初始化;按式 (26.5) 做批量更新,邻域半径从 4 逐步缩小到 1;最后计算量化误差、拓扑误差,打印标注映射、U 值和一个分量平面。
import numpy as np
rng = np.random.default_rng(5)
# ---------- 模拟 4 种市场状态下的 6 维日度特征 ----------
names = ["平稳上涨", "高波动下跌", "震荡", "危机"]
mu = np.array([[ 0.8, -0.8, -0.5, 0.6, -0.5, 0.0], # [动量, 波动, 相关性, 期限利差, 信用利差, 成交量]
[-0.6, 0.6, 0.5, -0.2, 0.4, 0.6],
[ 0.0, -0.1, 0.0, 0.0, 0.0, -0.6],
[-1.5, 1.8, 1.5, -1.0, 1.8, 1.5]])
lab = rng.choice(4, 2000, p=[.4, .25, .3, .05])
X = mu[lab] + rng.normal(0, 0.35, (2000, 6))
X = (X - X.mean(0)) / X.std(0) # 式(22.2)
# ---------- 六边形 9x5 网格, 约 sqrt(2000)=45 个神经元 ----------
cols, rows = 9, 5
pos = np.array([[c + 0.5 * (r % 2), r * np.sqrt(3) / 2] for r in range(rows) for c in range(cols)])
D = np.linalg.norm(pos[:, None] - pos[None], axis=2) # 神经元在映射上的距离
adjacent = (D > 0) & (D < 1.01) # 六边形: 内部神经元有 6 个邻居
# 线性初始化: 权值铺在前两个主成分张成的平面上
ev, evec = np.linalg.eigh(np.cov(X.T)); pc = evec[:, ::-1][:, :2] * np.sqrt(ev[::-1][:2])
g = (pos - pos.mean(0)) / pos.std(0)
W = X.mean(0) + g @ pc.T
def bmu(W, X): # 获胜神经元与次近神经元
d = np.linalg.norm(X[:, None] - W[None], axis=2); o = np.argsort(d, 1)
return o[:, 0], o[:, 1], d[np.arange(len(X)), o[:, 0]]
# 批量 SOM, 式(26.5): w_i = sum_q h_{i,i_q} p_q / sum_q h_{i,i_q}, 邻域半径逐步缩小
for radius in (4, 3, 2, 1, 1, 1):
win, _, _ = bmu(W, X)
h = (D[:, win] <= radius + 1e-9).astype(float) # 阶跃邻域函数(26.4), S x Q
W = (h @ X) / h.sum(1, keepdims=True)
win, second, dist = bmu(W, X)
print(f"量化误差 = {dist.mean():.3f} 拓扑误差 = {np.mean(~adjacent[win, second]):.3f}")
# 标注映射: 每个神经元标上命中样本中最多的状态; 命中直方图
hits = np.bincount(win, minlength=len(W))
tag = np.full(len(W), "·")
for i in range(len(W)):
if hits[i]: tag[i] = "ABCD"[np.bincount(lab[win == i], minlength=4).argmax()]
print("标注映射 (A=平稳上涨 B=高波动下跌 C=震荡 D=危机, ·=无命中):")
for r in range(rows - 1, -1, -1):
print(" " + (" " if r % 2 else "") + " ".join(tag[r * cols:(r + 1) * cols]))
purity = sum(np.bincount(lab[win == i]).max() for i in range(len(W)) if hits[i]) / len(X)
print(f"按神经元多数标签的纯度 = {purity:.3f}")
# U 矩阵的简化版: 每个神经元到其邻居权值的平均距离
U = np.array([np.linalg.norm(W[adjacent[i]] - W[i], axis=1).mean() for i in range(len(W))])
for k in range(4):
print(f" {names[k]:<5}: 所在神经元平均 U 值 = {U[win[lab == k]].mean():.3f}")
# 分量平面: 某个特征在映射上的分布(这里看"信用利差"列)
print("分量平面(信用利差, 权值 x10 取整):")
for r in range(rows - 1, -1, -1):
print(" " + (" " if r % 2 else "") + " ".join(f"{int(round(10 * W[r * cols + c, 4])):>3d}" for c in range(cols)))
关键输出:
量化误差 = 1.007 拓扑误差 = 0.052
标注映射 (A=平稳上涨 B=高波动下跌 C=震荡 D=危机, ·=无命中):
A A C C C C C B B
A A C C C C B B B
A A A C C C B B B
A A A A B B B B D
A A A A A B B B D
按神经元多数标签的纯度 = 0.973
平稳上涨 : 所在神经元平均 U 值 = 0.500
高波动下跌: 所在神经元平均 U 值 = 0.824
震荡 : 所在神经元平均 U 值 = 0.631
危机 : 所在神经元平均 U 值 = 1.343
分量平面(信用利差, 权值 x10 取整):
-11 -9 -2 1 1 0 1 2 5
-9 -5 0 0 1 1 5 7 6
-9 -6 -3 -1 0 2 6 8 17
-8 -5 -5 -4 2 5 7 17 26
-9 -7 -7 -6 -4 4 6 8 23
结果与案例四的各项分析一一对应。拓扑误差 5.2%,与原书的 3.7% 同一量级,说明映射保持了拓扑,可视化可信。标注映射上四种状态各占一块连续区域:平稳上涨在左,震荡在上中,高波动下跌在右,危机挤在右下角。纯度 97.3% 说明 SOM 在不知道状态标签的情况下找回了状态结构。危机样本所在神经元的 U 值最大(1.343),即它们与邻居差别很大,这就是 U 矩阵上"浅色山脊"的含义:危机是一个与其他状态隔开的区域,并且只占两个神经元,因为它只有约 5% 的样本。信用利差的分量平面从左到右单调升高、在右下角达到最大,说明它是区分危机和平稳状态的关键变量,相当于原书用分量平面找出区分类型 2 的变量。
使用时要注意:SOM 没有明确的最优准则,结果依赖于网格大小、初始化和邻域衰减方案。它更适合作为探索性工具,而不是直接当交易信号;在金融数据上还要检查簇在时间上是否稳定,例如用前一半数据训练、看后一半数据落到地图上的哪些位置。
本章小结
模式识别案例的流程是:用领域知识把海量原始信号压成少量有含义的特征;只在训练集中处理类别不平衡(重复少数类或加权误差),验证和测试集保持原始分布;输入归一化,tansig 输出目标设为 \(\pm0.76\);用 SCG 加提前停止训练,耐心期要足够长;用混淆矩阵和 ROC 验证,并按误判代价选阈值;用多次划分、多次初始化的蒙特卡洛评估得到误差的分布,识别持续误分的样本,并可让多个网络投票。聚类案例的流程是:先用散点图矩阵检查变量;归一化;神经元数约按 \(\sqrt N\) 设定;用前两个主成分做线性初始化;批量 SOM 把权值设为邻域内输入的平均,迭代次数少、与学习率无关,邻域逐步缩小;用量化误差、拓扑误差评估,用 U 矩阵看簇边界、命中直方图看频率、分量平面看变量作用。在量化中,前者对应稀有事件预警(注意过采样的位置、阈值的选择、评估的分布),后者对应市场状态地图。
| 概念 | 公式 / 要点 |
|---|---|
| 召回率(按目标类) | 对角元 / 列和 |
| 精确率(按输出类) | 对角元 / 行和 |
| 类别不平衡 | 训练集内重复少数类或加权误差;验证、测试集不动 |
| 蒙特卡洛评估 | 多次划分 × 多次初始化,报告误差均值与标准差 |
| SOM 神经元数 | 约 \(\sqrt N\) |
| 线性初始化 | 权值铺在前两个主成分平面上 |
| 序贯 SOM | \({}_i\mathbf w\leftarrow{}_i\mathbf w+\alpha h_{ii^*}(\mathbf p-{}_i\mathbf w)\) |
| 批量 SOM | \({}_i\mathbf w=\sum_qh_{ii_q}\mathbf p_q/\sum_qh_{ii_q}\) |
| 量化误差 / 拓扑误差 | 到获胜神经元的平均距离 / 获胜与次近不相邻的比例 |
| U 矩阵、命中直方图、分量平面 | 簇边界 / 样本频率 / 单个变量在映射上的分布 |
练习
基础
- 原书 1000 次蒙特卡洛的平均结果中,每个测试集约 11.6 个健康者、53.5 个患者,验证"平均测试误差约 9.5%"。 答案:误分数约 \(2.51+3.69=6.2\),总数 \(65.1\),\(6.2/65.1\approx9.5\%\)。
- 为什么重复健康记录只能在训练集中做?如果在划分前做,会发生什么? 提示:副本会同时出现在训练集和测试集,测试变成"考原题",结果被高估(本章代码中 AUC 从约 0.92 被抬高到 0.95)。
- 有 20,000 个数据点时,按经验规则 SOM 大约用多少个神经元?若数据增加到 80,000 呢? 答案:约 141;约 283(只增加一倍,而不是四倍)。
- 批量 SOM 为什么与学习率无关? 提示:式 (26.5) 中学习率在分子分母同时出现,相消。
- 量化误差可以通过增加神经元无限减小,那它还有什么用?什么情况下它没有意义? 提示:在神经元数固定时比较训练方案;神经元数不显著少于数据点数时没有意义。
进阶
- 在本章大跌预警代码中,不做重采样,而是在 ROC 曲线上找使"漏报代价 × FN + 误报代价 × FP"最小的阈值(设漏报代价是误报的 5 倍),与重复少数类的结果比较。
- 把大跌预警代码中的"重复少数类"换成加权误差(MLPClassifier 不支持样本权重时,可以用
sklearn.linear_model.LogisticRegression(class_weight="balanced")作对照),比较召回率、精确率和 AUC。 - 修改 SOM 代码:用随机初始化代替线性初始化,并只做原书那样的两次批量迭代(邻域 4、1),比较量化误差和拓扑误差。
- 在 SOM 代码中生成带时间顺序的状态序列(如马尔可夫链切换状态),把每天的获胜神经元连成轨迹,统计相邻两天获胜神经元在映射上的平均距离,在状态切换日和非切换日分别是多少?
- 用前一半数据训练 SOM,把后一半数据投到地图上,计算后一半数据的量化误差。如果后一半出现了训练期从未有过的状态,量化误差会怎样变化?这与第 22 章的新颖性检测有什么关系?
原书推荐习题:第 25、26 章为案例章,无习题。建议用原书数据 ekg_p.txt/ekg_t.txt 复现 1000 次蒙特卡洛划分,比较"重复少数类"与"加权误差",以及多网络投票的提升;用 cover_p.txt/cover_t.txt 比较线性初始化与随机初始化,以及不同网格尺寸下量化误差与拓扑误差的权衡。
原书对照
| 本章小节 | 原书章节 | PDF 页码 |
|---|---|---|
| 25.1.1 问题 | 25 Objectives;Description of Myocardial Infarction Recognition | p.941–942 |
| 25.1.2 数据与特征 | Data Collection and Preprocessing(47 个特征,类别不平衡) | p.943–946 |
| 25.1.3 结构与训练 | Selecting the Architecture;Training the Network(图 25.3–25.4) | p.946–947 |
| 25.1.4–25.1.5 验证 | Validation(图 25.5–25.8,蒙特卡洛) | p.947–950 |
| — | 第 25 章 Epilogue、Further Reading | p.951–952 |
| 25.2.1 问题 | 26 Objectives;Description of the Forest Cover Problem(表 26.1–26.2) | p.953–955 |
| 25.2.2 数据 | Data Collection and Preprocessing(图 26.1) | p.956–957 |
| 25.2.3 结构 | Selecting the Architecture(图 26.2) | p.957–958 |
| 25.2.4 训练 | Training the Network(式 26.1–26.5) | p.958–959 |
| 25.2.5 验证 | Validation(图 26.3–26.6) | p.959–962 |
| — | 第 26 章 Epilogue、Further Reading | p.963–964 |