
简介面向近红外光谱建模与机器学习入门的样本集划分实践材料聚焦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, dtypefloat) y np.asarray(y, dtypefloat).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(axis1) 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 分布画出来并排比一眼确认测试集不是一片空白才动手建模。这个动作花不了两分钟能挡掉大部分看起来很美的假成绩。希望帮到你。本文还有配套的精品资源点击获取