简介这份资源围绕SPXY样本划分法展开面向从事近红外光谱分析、化学计量学建模及机器学习数据预处理的研究生与科研人员帮助解决复杂或不均衡数据集中训练集与验证集如何更具代表性的问题。包内共4个文件以3个m脚本和1份pdf文献为主脚本可直接在MATLAB中运行pdf则给出SPXY样本划分法与蒙特卡罗交叉验证结合用于橘叶中橙皮苷含量测定的完整案例压缩包约291KB轻量便于快速查阅。资源将SPXY划分、KS检验与交叉验证思路串联起来读者可据此理解样本选择对模型精度的影响并借鉴脚本实现自己的划分流程。目前已有1825人学习下载适合需要复现光谱建模流程或优化样本集划分策略的读者参考。1. 样本集划分的隐形陷阱为什么随机切分让模型“虚高”很多人做回归或分类建模时拿到样本集的第一反应是train_test_split随机切一刀然后看着验证集上 0.95 的 R² 沾沾自喜。但如果你把同一批数据换个随机种子再切一次指标可能直接掉到 0.7。这不是玄学而是样本集划分本身出了问题——当样本在特征空间里分布不均匀时随机切分会让训练集和验证集的分布产生系统性偏移模型在验证集上的表现就成了“碰运气”。SPXYSample set Partitioning based on joint X-Y distances方法解决的正是这个痛点。它不像 KSKennard-Stone只考虑特征空间 X 的距离而是把特征变量 X 和目标变量 Y 同时纳入距离计算保证划分出的校正集能覆盖整个样本空间的“光谱-浓度”联合分布。关键词里的 SPXY、KS、交叉验证本质上是在回答同一个问题如何让有限的样本在训练和验证之间被“公平”地分配使模型评估结果可信、可复现。搞懂这套机制比调参更能决定你模型的真实上限。2. 从 KS 到 SPXY距离度量里为什么要塞进 Y2.1 KS 方法的本质在 X 空间里“最远优先”选点KS 算法的逻辑非常朴素先找欧氏距离最远的一对样本放进校正集然后每次从剩余样本里挑一个“离已选集合最近距离最大”的样本加入直到校正集达到目标数量。这个策略在特征空间里是合理的——它试图让校正集均匀覆盖整个 X 空间。但问题在于KS 完全无视 Y 的存在。假设你在做近红外光谱定量分析样本的 Y 是某种成分浓度。如果两个样本在光谱上差异很大但浓度几乎一样KS 会认为它们“很不同”而优先选入校正集反过来两个光谱相似但浓度差异显著的样本KS 可能把它们都丢进验证集。结果就是校正集在 Y 维度上分布稀疏模型学不到高浓度或低浓度区域的映射关系。常见的实现思路是KS 的距离矩阵只计算 X 的欧氏距离选点过程是一个贪心策略时间复杂度 O(n²)对于几百个样本的规模完全可接受。2.2 SPXY 的改进联合距离 d_xy 的构造SPXY 在 KS 的基础上做了一个关键改动把 Y 也当作一个维度参与距离计算。具体做法是先分别计算 X 空间和 Y 空间的欧氏距离然后做归一化处理最后相加得到联合距离。import numpy as np from scipy.spatial.distance import cdist def spxy_split(X, Y, n_train, random_state42): SPXY 样本集划分 X: 特征矩阵 (n_samples, n_features) Y: 目标值 (n_samples, 1) 或 (n_samples,) n_train: 校正集样本数 返回: 校正集索引, 验证集索引 X np.asarray(X, dtypefloat) Y np.asarray(Y, dtypefloat).reshape(-1, 1) n X.shape[0] # 分别计算 X 和 Y 空间的欧氏距离矩阵 dx cdist(X, X, metriceuclidean) dy cdist(Y, Y, metriceuclidean) # 归一化除以各自的最大距离消除量纲影响 dx_norm dx / dx.max() if dx.max() 0 else dx dy_norm dy / dy.max() if dy.max() 0 else dy # 联合距离 dxy dx_norm dy_norm # KS 式贪心选点 selected [] # 第一步选联合距离最远的一对 idx np.unravel_index(np.argmax(dxy), dxy.shape) selected.extend([idx[0], idx[1]]) # 剩余样本中迭代选择 remaining set(range(n)) - set(selected) while len(selected) n_train: # 对每个剩余样本计算它到已选集合的最小距离 min_dists [] for r in remaining: d_min min(dxy[r, s] for s in selected) min_dists.append((r, d_min)) # 选最小距离最大的那个 next_idx max(min_dists, keylambda x: x[1])[0] selected.append(next_idx) remaining.remove(next_idx) selected np.array(selected) test_idx np.array(list(set(range(n)) - set(selected))) return selected, test_idx这段代码的核心在dxy dx_norm dy_norm这一行。归一化是必须的因为 X 可能是几十维的光谱数据Y 只是一维浓度值如果不归一化X 的距离数值会完全淹没 Y 的贡献。归一化之后X 和 Y 对联合距离的权重各占一半这是 SPXY 的默认策略。参数说明n_train决定校正集大小通常取总样本的 70%80%random_state在这个确定性算法里其实用不到但保留是为了接口兼容。选点过程是确定性的同样的输入永远得到同样的划分这一点比随机切分强太多。2.3 联合距离的几何直觉把 X 和 Y 想象成两个独立的空间SPXY 做的事情是在这两个空间的“笛卡尔积”里做最远优先采样。一个样本如果 X 离群且 Y 也离群它的联合距离会很大优先被选入校正集如果只是 X 离群但 Y 居中联合距离被 Y 拉低可能不会优先选。这样选出来的校正集在 X-Y 联合分布上是覆盖最广的模型训练时见过的“模式”最全。我一般会这么理解KS 是在 X 空间撒网SPXY 是在 X-Y 空间撒网。对于光谱定量、传感器标定这类 Y 与 X 有强关联的场景SPXY 的划分结果通常能让交叉验证的 RMSE 更稳定。3. 交叉验证与划分的耦合为什么划分完还要再切3.1 校正集内部还要做交叉验证SPXY 划分出校正集和验证集之后很多人直接在校正集上训练、在验证集上评估然后就结束了。但这里有个隐藏问题模型的超参数怎么定如果用验证集来调参验证集就“泄漏”了最终评估指标会偏乐观。正确的做法是在校正集内部再做交叉验证。比如 5 折交叉验证把校正集分成 5 份每次用 4 份训练、1 份验证循环 5 次取平均指标用来选超参数。定好超参数后再用整个校正集重新训练最后在独立的验证集上评估一次。这个流程里SPXY 负责“粗分”交叉验证负责“细分”两者配合才能得到可信的泛化误差估计。from sklearn.model_selection import KFold from sklearn.cross_decomposition import PLSRegression from sklearn.metrics import mean_squared_error def cv_pls(X_train, Y_train, max_components15, n_splits5): 在校正集内部做 K 折交叉验证选择 PLS 最优主成分数 kf KFold(n_splitsn_splits, shuffleTrue, random_state0) rmse_list [] for n_comp in range(1, max_components 1): fold_rmse [] for train_idx, val_idx in kf.split(X_train): pls PLSRegression(n_componentsn_comp) pls.fit(X_train[train_idx], Y_train[train_idx]) pred pls.predict(X_train[val_idx]) fold_rmse.append( np.sqrt(mean_squared_error(Y_train[val_idx], pred)) ) rmse_list.append(np.mean(fold_rmse)) best_n np.argmin(rmse_list) 1 return best_n, rmse_list这里n_splits5是经验值样本量少于 50 时可以用留一法LOO但 LOO 的计算量是 O(n)样本多的时候会慢。max_components一般不超过 15PLS 主成分数太多会过拟合。rmse_list记录了每个主成分数对应的交叉验证 RMSE选最小值对应的主成分数作为最终模型参数。3.2 划分比例与交叉验证折数的联动SPXY 划分时校正集占多少、验证集占多少和交叉验证的折数是有耦合的。如果校正集只有 30 个样本做 5 折交叉验证每折只有 6 个样本方差会很大这时候要么减少折数到 3 折要么增大校正集比例到 85%。常见的经验规则是校正集样本数至少是特征数的 510 倍交叉验证每折的验证样本数不少于 5 个。如果样本总量实在少可以考虑重复交叉验证repeated K-fold把随机种子换几次取平均降低划分随机性带来的波动。提示SPXY 划分本身是确定性的但交叉验证的折划分如果用了shuffleTrue每次运行结果会不同。做最终报告时要么固定随机种子要么用重复交叉验证取平均。4. 避坑与排查SPXY 落地时的五个翻车现场4.1 现象SPXY 划分后验证集指标比随机划分还差原因Y 的归一化没做或者 X 和 Y 的量纲差异过大导致联合距离实际上只反映了 X 的距离SPXY 退化成 KS。另一种可能是 Y 本身噪声很大把噪声也当成“距离”参与选点选出的校正集在真实 Y 上并不均匀。解决检查dx.max()和dy.max()的数量级如果相差超过两个数量级必须归一化。如果 Y 噪声大可以先对 Y 做平滑或剔除异常值再跑 SPXY。4.2 现象校正集和验证集的 Y 均值差异很大原因SPXY 的贪心选点策略倾向于选极端样本导致校正集里高浓度和低浓度样本偏多中间浓度样本被挤到验证集。这在样本量少的时候尤其明显。解决划分完成后画一下校正集和验证集的 Y 分布直方图如果均值差异超过一个标准差考虑改用分层 SPXY先按 Y 分箱每个箱内做 SPXY或者手动调整划分比例。4.3 现象交叉验证的 RMSE 曲线剧烈震荡原因折数太多而每折样本太少或者样本本身有批次效应不同折之间的数据分布差异大。SPXY 只保证了校正集整体的覆盖度没有保证每折内部的分布一致性。解决减少折数到 35 折或者在交叉验证之前先对校正集做一次 SPXY 排序按排序结果均匀分折而不是随机分折。4.4 现象SPXY 运行极慢几百个样本跑了几分钟原因cdist计算的是全距离矩阵复杂度 O(n²)贪心选点又是 O(n²) 的循环。样本量到几千时Python 循环会成为瓶颈。解决用 NumPy 向量化选点过程或者改用scipy.spatial.distance.cdist的metricsqeuclidean减少开方运算。样本量超过 5000 时可以考虑近似算法比如先聚类再在簇间做 SPXY。4.5 现象换了随机种子SPXY 结果不变但交叉验证结果变了原因SPXY 本身是确定性的但交叉验证的KFold如果设了shuffleTrue折划分会随种子变化。这不是 SPXY 的问题是交叉验证的随机性。解决如果希望整个流程完全可复现把交叉验证的shuffle设为False或者固定random_state。但要注意shuffleFalse时如果样本有顺序趋势折之间的分布会有偏。5. 设计取舍确定性划分与随机性评估的平衡SPXY 的核心价值在于“确定性”——同样的输入永远得到同样的划分这让不同模型之间的对比变得公平。但确定性也意味着它无法通过多次随机划分来估计划分本身带来的方差。随机切分虽然每次结果不同但你可以跑 100 次取平均得到一个更稳健的评估。我的做法是用 SPXY 做一次确定性划分固定校正集和验证集用于模型选型和超参数调优然后在最终报告时用随机切分跑 2050 次观察指标分布把 SPXY 的结果放在分布里对比。如果 SPXY 的指标落在随机分布的 50% 分位附近说明划分没有引入系统性偏差如果落在极端位置就要检查是不是 SPXY 的联合距离权重需要调整。划分策略确定性Y 空间覆盖计算复杂度适用场景随机切分否不保证O(n)样本量大、分布均匀KS是不保证O(n²)仅关注 X 空间覆盖SPXY是保证O(n²)X-Y 强关联、样本量中等分层 SPXY是更强O(n² kn)Y 分布极不均匀还有一个取舍是 X 和 Y 的权重。默认各占 0.5但如果你的 X 特征维度很高且信噪比低可以调低 X 的权重让 Y 主导选点反之如果 Y 噪声大就调高 X 的权重。这个权重没有理论最优值需要根据具体数据做敏感性分析。从那以后我每次做样本集划分都强制走一遍“SPXY 划分 → 校正集内交叉验证 → 独立验证集评估 → 随机划分对比”的流程再也不敢直接train_test_split了。希望帮到你。本文还有配套的精品资源点击获取