ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SPXY样本集划分:联合距离、KS对比与评估避坑指南

SPXY样本集划分:联合距离、KS对比与评估避坑指南

简介:面向近红外光谱建模与机器学习入门的样本集划分实践材料,聚焦SPXY样本划分法及其与蒙特卡罗交叉验证结合时的模型优化思路,适合需要处理训练集、验证集与测试集划分,并评估模型泛化能力的高年级本科生、研究生和算法初学者。压缩包共4个文件,约291KB,以3个MATLAB脚本和1份PDF文献为主:脚本覆盖SPXY划分、随机划分与KS检验等环节,文献结合橘叶中橙皮苷含量测定案例,说明光谱数据预处理、样本选择及交叉验证的完整流程。已有1827人学习,反映该主题在光谱定量分析与模型验证中关注度较高。读者可据此理解SPXY如何兼顾特征空间与浓度分布,比较不同划分策略对预测精度的影响,并借助KS检验判断预测分布与真实分布的差异,为参数调优和最终评估提供可复用的脚本与实验参照。

1. 一份「自己出题自己考」的成绩单:样本集划分到底在分什么

小时候都干过这种事:把练习册的答案抄一遍,再把同一套题当考试做,分数当然漂亮,可换一套题就原形毕露。模型也是这个毛病——喂给它见过的样本,它答得又快又准;真正要看的是它面对没见过的样本还能不能站住。

样本集划分要解决的正是这个问题:把有限的样本切成几份,一份用来“学”,一份藏起来“考”。SPXY 是其中比较讲究的一种做法,全称叫“基于 X 与 Y 联合距离的样本集划分”。名字唬人,说白了它只是在决定“哪些样本去当练习题、哪些去当考卷”时,比随机抓阄多考虑了一件事——样本的标签值也得分布均匀。搞懂它,你才能判断自己那份模型成绩单,到底是真本事还是背答案。

2. 为什么不能随手一划:KS 与 SPXY 差在「看不看 Y」这一刀

很多人第一次做划分,直接train_test_split一下,比例 8:2,跑完指标 0.98,开心得不行。等换一批新样本进来,误差翻了三倍,才发现当初那份成绩单是掺了水的。问题往往不在模型,而在“分”这一步就已经埋下了雷。

2.1 随机划分翻车的三个典型场景

第一个场景是浓度断层。一批样本的参考值集中在中间某个窄区间,只有零星几个落在两端。随机切 20% 做测试集,很可能低端那几个样本一个都没被抽中,于是模型在低浓度段的表现完全无从验证。指标好看,是因为考卷里压根没有难题。

第二个场景是批次漂移。样本按时间顺序采集,仪器上午和下午的状态有细微差别。如果直接按顺序切前 80% 训练、后 20% 测试,测试集就整段落在“下午”,测出来的其实是跨批次泛化能力。这未必是错,但你得清楚自己在测什么,而不是把结果当成“模型精度”四个字贴到报告上。

第三个场景是平行样泄漏。同一样品测了三次,或者为了扩样本量做了轻微扰动,这些“亲戚样本”被随机分到不同集合里。测试集里坐着一堆训练集的近亲,指标自然虚高。这类翻车最隐蔽,因为流程上每一步都合规,只有把样本的来源捋一遍才会发现。

随机划分的问题在于它不保证任何事。它只保证每个样本有固定概率落进某一侧,至于落完之后两边的分布像不像,它不管。样本量上千、分布本身就均匀的时候,这种“不管”影响不大;样本量只有一百出头、标签范围又拉得很开的时候,一次不走运的随机,就足以让整篇结论作废。

2.2 KS 方法:只看 X 距离的均匀撒点

KS 全称 Kennard-Stone,比 SPXY 早出现很多年,思路朴素得很:样本在特征空间里铺得越开,模型见到的情况就越全。它只关心 X,也就是光谱、图像特征、工艺参数这一侧。

做法是这样的:先算出所有样本两两之间的 X 距离,挑出距离最远的那一对作为种子;然后每一轮,对每个还没入选的样本,算出它到“已选集合”里所有样本的距离,取其中最小的那个,再从所有候选里挑出这个最小值最大的样本,加入已选集合。重复到训练集凑够数为止。

这套“最大最小距离”的逻辑,效果是让选出来的样本彼此尽量疏远,像往一张地图上撒点,先撒最边上的,再往中间填空。好处是确定性算法,没有随机种子,同一个数据集跑一百遍结果都一样,可复现;坏处是它眼里只有 X。如果两个样本在 X 空间离得很远,但标签值几乎相同,KS 会优先把它们都选进训练集,白白占掉名额。

更麻烦的情况是 X 和 Y 的关系不单调。比如某段光谱区间对浓度不敏感,两个浓度差很多的样本,光谱却长得几乎一样。KS 看不出这个差别,它按 X 距离挑,很可能把一整段浓度区间的样本全漏掉。等到测试集里冒出一个该区间的样本,模型只能靠外推硬猜。

2.3 SPXY 方法:把 Y 也拉进距离公式

SPXY 的关键改动只有一处:距离不再只算 X,而是把 Y 也算一份,各自归一化后相加。

$$d_{xy}(p,q)=\frac{d_x(p,q)}{\max d_x}+\frac{d_y(p,q)}{\max d_y}$$

$d_x$ 是样本 $p$、$q$ 在特征矩阵上的欧氏距离,$d_y$ 是它们在参考值上的欧氏距离。单变量时 $d_y$ 就是两个数值之差的绝对值,多变量时按欧氏距离算。

分母上的两个 $\max$ 是整套方法里最容易被忽略、也最不能省的一步。原始光谱的强度动辄几千,算出来的 $d_x$ 可能是几百上千;而参考值如果是含量百分比,$d_y$ 可能只有零点几。不归一化直接相加,Y 的贡献会被 X 彻底淹没,SPXY 就退化成了 KS。除以各自的最大值之后,两项都被压进 0 到 1 之间,权重才对等。

换个说法:KS 挑样本时只问“这两个样本长得像不像”,SPXY 还要多问一句“它们的答案差多少”。长得不像、答案又差得多的样本,SPXY 会优先送进训练集,因为这类样本信息量最大。

2.4 一个公式看清两者的分水岭

把三种常见做法摆在一起,差别一目了然。

维度随机划分KSSPXY
挑选依据无,纯概率仅 X 距离X 与 Y 联合距离
结果是否可复现需固定随机种子是是
训练集覆盖 Y 范围不保证不保证更容易覆盖两端
额外计算开销几乎没有一个 X 距离矩阵两个距离矩阵
适合的场面样本量大、分布本身均匀只关心特征空间覆盖样本少、参考值跨度大

分水岭就在中间那一列。KS 和 SPXY 的流程几乎一模一样,代码改一行就能互换,但选出来的样本集合可能差得很远。判断该用哪个,看的不是哪个名字更时髦,而是你的 Y 分布是不是均匀——Y 本来就均匀,KS 够用;Y 有明显断层或者两端样本稀少,SPXY 那额外一份距离就算得值。

3. SPXY 走一遍:从距离矩阵到挑出第一批样本

公式看懂了,落到操作上还有几步容易踩坑的地方。这一章把流程拆开走一遍,顺便说清楚每个参数改动的后果。

3.1 第一步永远是看数据,不是算距离

拿到一份样本集,先做三件事:看样本量、看 Y 的分布、看有没有重复或近似重复的样本。这三件事决定了后面所有参数怎么定。

样本量决定测试集能有多大。总共 60 个样本,按 8:2 分,测试集只有 12 个,任何一个样本预测偏了,指标就大幅波动。这时候更合理的比例是 2:1 甚至 3:1,把测试集撑到 20 个以上。样本量上千,比例反倒可以放松,因为统计量本身已经稳了。

Y 的分布决定要不要用 SPXY。把 Y 画成直方图,如果两端稀稀拉拉、中间堆成一座山,随机划分和 KS 都有较大概率漏掉两端,SPXY 值得上。如果 Y 近似均匀分布,那 KS 省一半计算量,效果差不多。

重复样本要在划分之前处理掉。同一样品测三次,这三条记录必须分进同一个集合,否则就是前面说的泄漏。做法很简单:给每个原始样品编一个组号,划分时按组号分,而不是按行号分。

还有一步常被跳过:X 的各列要不要标准化。如果特征里既有光谱强度(量级上千)又有温度(量级几十),直接算欧氏距离,光谱会独占话语权,温度等于没参与。用欧氏距离之前,把 X 各列标准化到同一量级,这个距离才有意义。SPXY 公式里那层归一化解决的是“X 距离和 Y 距离之间”的量级差,解决不了“X 内部各列之间”的量级差,两件事不能混。

3.2 选出起手的那一对最远样本

算完联合距离矩阵,第一步是在整个矩阵里找最大值。这一对样本就是种子,直接进训练集。

为什么从最远的一对开始?因为它们代表了这个数据集能覆盖的边界。先把边界占住,后面的样本才是在边界之间填空,整个训练集的覆盖范围从第一轮就定下了基调。

这里有个细节:如果数据里存在测量错误的离群样本,它很可能就是那个“最远点”,第一轮就被选进训练集。这是好事还是坏事,取决于那个离群点是真实极端情况还是录入错误。真实极端情况该进训练集,模型得见过;录入错误进了训练集,等于往模型嘴里塞了一口沙子。所以划分之前先看一眼最远的那几对是谁,值不值得信任,这一步花不了五分钟,能省掉后面很多返工。

3.3 逐轮挑选:谁离已选集合最远就选谁

种子定下之后进入循环。每一轮做三件事:算每个未选样本到已选集合里所有样本的距离、取最小值、在所有候选里挑这个最小值最大的那个。

注意这里是“最小距离的最大值”,不是“最大距离的最大值”。区别在于:如果只看最大距离,一个离某个已选样本很远的样本会被优先选中,但它可能同时离另一个已选样本很近,选进来等于在密集区又添了一个点,没有增加覆盖。取最小距离,意思是“这个候选离已选集合最近的那个点有多远”,这个值越大,说明它离整个已选集合越疏远,选它才真正扩大覆盖。

这个机制带来一个隐含后果,值得单独说一句:SPXY 选出的顺序,本质上是“从边缘往中间收”。排在前面的样本偏边缘,越往后越靠近密集区。如果你的训练集取前 70%,测试集就是剩下的 30%,那测试集大概率集中在密集区,几乎不含极端样本。这意味着模型的外推能力没有被检验,测出来的指标反映的是内插表现。

这不是 SPXY 的缺陷,是它的特性,但你得知道。如果业务上关心的是“超出历史范围的新样本能不能预测准”,那测试集里必须人为保留几个极端样本,不能全交给算法去分。做法是从两端各留一两个样本不参与划分,直接塞进测试集,剩下的再走 SPXY。这一手有点土,但管用。

3.4 完整实现与参数逐个拆开看

下面这段是标准实现,把前面几节的逻辑落成代码。

import numpy as np def spxy(X, y, n_train): """基于 X-Y 联合距离的样本集划分 X: 特征矩阵,形状 (n_samples, n_features) y: 参考值,形状 (n_samples,) 或 (n_samples, n_targets) n_train: 训练集样本数 返回: (训练集下标, 测试集下标) """ X = np.asarray(X, dtype=float) y = np.asarray(y, dtype=float).reshape(len(X), -1) # 1. 分别计算 X 空间与 Y 空间的欧氏距离矩阵 dx = np.sqrt(((X[:, None, :] - X[None, :, :]) ** 2).sum(axis=-1)) dy = np.sqrt(((y[:, None, :] - y[None, :, :]) ** 2).sum(axis=-1)) # 2. 各自除以本矩阵最大值,压到 0~1,让两项权重对等 d = dx / dx.max() + dy / dy.max() n = len(X) # 3. 起手:联合距离最大的一对样本 i, j = np.unravel_index(np.argmax(d), d.shape) selected = [int(i), int(j)] # 4. 循环:每轮选“到已选集合最小距离”最大的候选 while len(selected) < n_train: rest = [k for k in range(n) if k not in selected] # d[np.ix_(rest, selected)] 取出候选行 × 已选列的子矩阵 min_to_selected = d[np.ix_(rest, selected)].min(axis=1) selected.append(rest[int(np.argmax(min_to_selected))]) test = [k for k in range(n) if k not in selected] return np.array(selected), np.array(test)

几个参数的含义和改动后果:

n_train决定训练集大小。它和测试集大小是互补的,设成 0.7 倍样本数就是 7:3。改这个值会直接改变训练集覆盖的疏密程度,n_train 越小,训练集越偏向边缘,模型见到的中段样本越少。

dy的算法对单变量和多变量不一样。单变量时用绝对差也行,多变量时必须用欧氏距离,否则多个目标值之间没法合成一个标量。如果你的 y 是多列输出,reshape(len(X), -1)那一步不能省,否则广播会出错。

dx.max()和dy.max()这两个分母,如果某个矩阵全零(所有样本特征完全一致),会除零报错。实际数据里很难出现,但做小规模试验时可能碰到,加个判断更稳妥。

整个函数返回的是下标,不是数据本身。这个设计是为了让原始数据保持不动,训练集和测试集在需要的时候按下标取,避免复制带来的内存开销,也避免改动顺序后对不上号。

跑完之后别急着建模,先把两组样本的 Y 值分布画出来对比一眼。理想状态下,训练集的 Y 覆盖整个范围,测试集的 Y 落在范围之内且分布均匀。如果测试集某一段完全空白,说明划分还有问题,得回去调 n_train 或者手动补样本。

4. 易混与误区:SPXY、KS、交叉验证别搅成一锅

这三个词经常在同一段话里出现,功能却完全不在一个层面上。把它们分开,比记住公式更重要。

4.1 误区一:SPXY 是一种建模算法

易混点:名字里没有“划分”二字,看起来和 PLS、SVR 这些算法是并列关系,很多资料里也确实把它们排在一起讲。

为什么混:SPXY 总是出现在建模流程的上下文里,前后文都在聊模型精度,很容易被当成“又一种建模方法”。

怎么区分:看它的输出。SPXY 输出的是两组下标——训练集是哪几行、测试集是哪几行,它不产生任何预测值,也不拟合任何参数。建模算法吃的是数据,吐的是预测;SPXY 吃的是数据,吐的是“怎么分”。它跑完之后,模型还是那个模型,只是喂进去的样本换了。所以 SPXY 的效果不能单独评价,只能通过“用了它之后模型在测试集上的表现”间接体现。

顺带提一句拼写。有些资料里写成sxpy,其实就是 SPXY 的手误,两者指的是同一个东西。搜索的时候两种写法都试一下,能多找到一些材料。

4.2 误区二:KS 和 SPXY 只差一个字母,可以随便换

易混点:流程一模一样,代码只改一行距离矩阵,结果看起来也差不多,于是很多人默认两者等价。

为什么混:在 Y 分布本来就均匀的数据集上,两者的选择结果确实高度重合,跑出来的模型精度也相差无几。这时候换不换,指标上看不出来。

怎么区分:把两份划分结果各自的 Y 直方图拉出来比。如果 Y 分布均匀,两者几乎一样,用 KS 更省事;如果 Y 有两端稀薄、中间堆积的情况,SPXY 的训练集会明显更均衡地覆盖两端,KS 则可能在中段扎堆。判断标准不在算法本身,在你的数据长什么样。另外,KS 的计算量少一个距离矩阵,样本上万时这个差别不小,值得纳入考虑。

4.3 误区三:做了交叉验证就不用留测试集

易混点:k 折交叉验证给出的分数看起来很严谨,每一折都当过验证集,感觉已经把泛化能力测透了。

为什么混:两者测的都是“模型在没见过的样本上表现如何”,听起来像同一件事的两种叫法。

怎么区分:交叉验证发生在训练集内部,用途是选超参数、比模型结构;测试集是完全不参与任何决策的一批样本,只在最后用一次。区别在“用了几次”。交叉验证会被反复用——试十组参数,每组都跑一遍交叉验证,取分数最高的那组。这个过程里,验证集的信息已经通过你的选择渗进了模型。测试集如果也这么用,它就不再是独立的考卷,而是变成了第二个验证集,指标会越来越乐观,越来越不真实。正确的顺序是:训练集内部做交叉验证定参数,模型定死之后,测试集只跑一次,跑完就封存。

4.4 误区四:测试集指标不好看,调一调再测一次

易混点:第一次测出来误差偏大,回去改了改特征,再测一遍,好多了,于是认为模型进步了。

为什么混:这个操作在流程上完全跑得通,代码也不会报错,结果也确实变好了,看不出问题。

怎么区分:问题在于“变好”的原因。你看到了测试集的结果,然后根据这个结果改了模型——这一步之后,测试集的信息就已经泄漏进模型了。第二次的指标不再是无偏估计,而是“针对这份考卷调过参”的成绩。改一次还能接受,改十次之后,那个指标基本没什么参考价值了。真要做这一步,就把原测试集降级成验证集,另外再划一批从来没碰过的样本当新测试集。样本不够划怎么办?那就别调,接受第一次的结果,或者承认这份数据的结论强度有限。

4.5 误区五:比例照着 7:3 抄就不会错

易混点:到处都在写 7:3、8:2,仿佛这是某种标准答案。

为什么混:大多数教程的示例数据样本量都在几百上千,那个量级下比例怎么定影响都不大,于是比例被当成了固定参数。

怎么区分:比例的本质是“测试集要装多少个样本才够支撑一个可信的判断”。样本量 50 的时候,7:3 意味着测试集 15 个,一个样本预测偏了,指标就跳一大截,这时候 2:1 让测试集到 17 个也没什么本质改善,反而训练集更紧张。样本量 50 这种小数据集,更实际的做法是留一交叉验证或者重复多次随机划分取平均,而不是纠结一个固定比例。反过来,样本量上万的时候,测试集留 10% 就是一千个,统计上足够稳,多留纯属浪费训练数据。比例跟着样本量和任务难度走,不跟着教程走。

5. 什么时候该用 SPXY,什么时候该收手

把前面几章的意思收成一句话:SPXY 是解决“Y 分布不均、样本又少”这个组合问题的工具,脱离这个前提,它的优势就不成立。

下面这张表可以直接对着自己的数据比。

你的情况建议做法
样本量一百出头,参考值跨度大、两端样本稀少SPXY,训练集比例放宽到 2:1 或 3:1
样本量几百以上,Y 分布本身均匀KS 或随机划分都行,省计算
样本按批次、按时间采集,存在系统漂移先按批次分层,再在每层内部用 SPXY
存在同一样品的多次测量按样品分组划分,绝不按行号随机
关心的是超出历史范围的外推能力SPXY 划分后,手动从两端各抽样本补进测试集
样本量不到 30别硬分测试集,用留一交叉验证
Y 是多列输出且量纲差异大先对每列 y 标准化,再算联合距离

需要收手的场面主要有两类。

一类是数据量本来就撑不起划分。样本二三十个,硬切出 20% 做测试集,测试集只有五六个样本,任何一个偏了,误差就翻倍。这时候与其纠结 SPXY 还是 KS,不如老老实实做留一交叉验证,把每一个样本都当过一次验证样本,虽然计算量大一点,但结论稳得多。划分方法的价值建立在“测试集样本数够多”这个前提上,前提不成立,方法再精巧也白搭。

另一类是 Y 分布本身很均匀。这时候 SPXY 多算的那份 Y 距离,几乎不会改变选择结果,付出的计算成本换不来对应的收益。样本量上千的时候,两个距离矩阵的内存占用也不是小数目。用 KS,甚至用固定种子的随机划分,效果一样,代码还短。

还有一个需要主动放弃的场景:如果数据里存在大量测量错误、标签可疑的样本,SPXY 会把它们优先挑出来塞进训练集——因为它挑选的依据就是“最远”,而错误样本往往正好最远。这时候该先做的是数据清洗,而不是换划分方法。划分解决的是“怎么分得公平”,解决不了“数据本身靠不靠得住”。

具体到一个例子:某次做近红外光谱的定量模型,样本 80 个,参考值从低到高分三段,中间一段样本占了七成。第一次用随机划分,测试集里一个高值样本都没有,交叉验证的分数漂亮,但模型一上真实样品就系统性偏低。换成 SPXY 之后,训练集的 Y 覆盖了全段,测试集也落在合理范围内,指标从 0.98 掉到了 0.87——数字变难看了,但这次的 0.87 是可信的。如果当时只看那个 0.98 就交付,后面踩的坑只会更深。

从那以后我每次分完样本集,都会先把两组样本的 Y 分布画出来并排比一眼,确认测试集不是一片空白,才动手建模。这个动作花不了两分钟,能挡掉大部分看起来很美的假成绩。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进