☰
SPXY样本集划分:解决模型验证集翻车的隐形陷阱
2026/10/16 11:56:34 网站建设 项目流程

简介:这份资源围绕SPXY样本划分法展开,面向从事近红外光谱分析、化学计量学建模及机器学习数据预处理的研究生与科研人员,重点解决样本集如何划分才能兼顾代表性与模型泛化能力的问题。压缩包共4个文件,以3个m脚本和1份pdf文献为主,整体约291KB,脚本可直接在MATLAB中运行,文献则给出方法原理与应用案例。内容将SPXY样本划分法与蒙特卡罗交叉验证结合,并涉及KS检验,用于评估预测分布与真实分布的一致性,读者可据此复现橘叶中橙皮苷含量测定的完整建模流程,理解训练集、验证集与测试集的划分策略,掌握模型调优与最终评估的衔接方式。目前已有1825人学习下载,适合需要快速上手光谱建模样本划分的读者参考借鉴。

1. 样本集划分的隐形陷阱:为什么你的模型总在验证集上翻车

做回归或分类建模时,很多人把八成精力花在调模型上,却在样本集划分这一步随手一个train_test_split就完事。结果模型在训练集上表现漂亮,一到新数据就崩。问题往往不在算法,而在划分本身——随机划分会让相近的样本同时落进训练集和验证集,造成信息泄漏,验证分数虚高。SPXY(Sample set Partitioning based on joint X-Y distances)就是为解决这个问题设计的:它同时考虑自变量 X 和因变量 Y 的距离,让选出的样本在两者空间里都均匀分布。这套方法在近红外光谱、拉曼光谱、高光谱等小样本高维数据的建模里用得很多,适合做定量校正模型、需要代表性样本子集的从业者。KS(Kennard-Stone)只按 X 空间距离选样,SPXY 在它基础上把 Y 也纳入距离计算,这是两者最核心的差别。

2. SPXY 与 KS 的算法拆解:距离矩阵到底怎么算

2.1 从 KS 到 SPXY 的那一步扩展

KS 算法的逻辑很直白:先算出所有样本两两之间的欧氏距离,挑出距离最远的一对作为初始集合,然后每次从剩余样本里选一个“到已选集合最小距离”最大的样本,直到选够数量。它保证选出的样本在 X 空间里铺得最开,但完全不看 Y。问题在于,如果两个样本的 X 很接近但 Y 差很多,KS 可能只选其中一个,另一个的信息就丢了。

SPXY 的做法是在距离定义上动刀。它把 X 距离和 Y 距离分别归一化后相加,得到联合距离:

d_xy(i,j) = d_x(i,j)/max(d_x) + d_y(i,j)/max(d_y)

其中d_x是样本 i 和 j 在自变量空间的欧氏距离,d_y是因变量之差的绝对值(单变量)或欧氏距离(多变量)。归一化是为了让两个距离量纲一致,否则 Y 的尺度会直接压过 X。选样流程和 KS 完全一样,只是距离矩阵换了。这样选出来的样本,既在 X 空间分散,又在 Y 空间覆盖均匀,对建立稳健的校正模型更有利。

2.2 距离矩阵的计算与内存边界

实现时第一个要面对的就是内存。假设有 2000 个样本,距离矩阵是 2000×2000 的浮点数组,约 32 MB,还能接受。但如果样本量到 2 万,就是 3.2 GB,普通机器直接爆。常见做法是分块计算,或者用scipy.spatial.distance.cdist配合pdist只存上三角。我一般会先估一下样本量,超过 5000 就改用分块策略,避免中途 MemoryError。

下面是一个可直接复用的 SPXY 实现,输入 X 是特征矩阵,y 是目标向量,n_select 是要选出的样本数:

import numpy as np from scipy.spatial.distance import cdist def spxy(X, y, n_select): """ SPXY 样本集划分 X: (n_samples, n_features) 自变量矩阵 y: (n_samples,) 或 (n_samples, n_targets) 因变量 n_select: 需要选出的样本数量 返回: 选中样本的索引列表 """ n = X.shape[0] # 计算 X 空间距离矩阵 dx = cdist(X, X, metric='euclidean') # 计算 Y 空间距离矩阵,y 需为二维 y2 = y.reshape(-1, 1) if y.ndim == 1 else y dy = cdist(y2, y2, metric='euclidean') # 归一化,避免除零加一个小量 dx_norm = dx / (dx.max() + 1e-12) dy_norm = dy / (dy.max() + 1e-12) dxy = dx_norm + dy_norm # 选初始最远的一对 idx = np.unravel_index(np.argmax(dxy), dxy.shape) selected = [idx[0], idx[1]] # 候选集 candidates = list(set(range(n)) - set(selected)) # 迭代选样 while len(selected) < n_select: # 每个候选样本到已选集合的最小距离 min_dist = np.min(dxy[np.ix_(candidates, selected)], axis=1) # 选最小距离最大的那个 best = candidates[np.argmax(min_dist)] selected.append(best) candidates.remove(best) return selected

这段代码里几个参数值得说清楚。metric='euclidean'是默认选择,如果 X 是光谱数据,通常不需要换;如果特征量纲差异大,建议先做标准化再传进来,否则距离会被大量纲特征主导。1e-12是防止 max 为零时的除零保护,实际数据里几乎不会触发,但加上更稳。np.ix_那行是花式索引,用来取候选行和已选列的子矩阵,比循环快很多。选样循环的时间复杂度是 O(n_select × n × n_select),样本量上万时建议用 KDTree 或 BallTree 加速最近邻查询,不过 SPXY 本身选样数通常不大,直接算也能接受。

2.3 划分比例与随机性的取舍

SPXY 是确定性算法,同样的输入永远得到同样的划分,这对复现实验是好事,但也意味着你没法通过换随机种子来观察划分稳定性。常见做法是先用 SPXY 选出 70% 作为训练集,剩余 30% 作为验证集;如果要做交叉验证,可以在 SPXY 选出的训练集内部再做 K 折。注意 SPXY 本身不保证选出的子集在 Y 上严格分层,如果 Y 的分布极度偏斜,比如回归里存在少量极端值,最好先检查选出的样本 Y 范围是否覆盖了全体。我一般会画一个 Y 的箱线图对比,确认训练集和验证集的 Y 分布没有明显偏移。

3. 交叉验证与 SPXY 的配合:怎么验证划分是否合理

3.1 划分后的验证流程

选完样本只是第一步,关键是验证这个划分能不能支撑可信的模型评估。标准流程是:用 SPXY 选出的训练集建模,在独立验证集上预测,计算 RMSE、R² 等指标。但单次划分仍有偶然性,更稳的做法是在 SPXY 选出的训练集内部做 K 折交叉验证,用交叉验证的均值和方差来判断模型是否稳定。如果交叉验证的 RMSE 波动很大,说明训练集内部样本分布不均,可能需要调整 SPXY 的选样数量或改用分层 SPXY。

下面是一个结合 SPXY 和 5 折交叉验证的完整评估示例,用偏最小二乘回归(PLS)作为模型:

import numpy as np from sklearn.cross_decomposition import PLSRegression from sklearn.model_selection import KFold from sklearn.metrics import mean_squared_error, r2_score def evaluate_spxy_pls(X, y, n_components=10, test_ratio=0.3): """ 用 SPXY 划分后做 PLS 建模,训练集内 5 折交叉验证 """ n = X.shape[0] n_test = int(n * test_ratio) n_train = n - n_test # SPXY 选训练集 train_idx = spxy(X, y, n_train) test_idx = list(set(range(n)) - set(train_idx)) X_train, y_train = X[train_idx], y[train_idx] X_test, y_test = X[test_idx], y[test_idx] # 训练集内 5 折交叉验证 kf = KFold(n_splits=5, shuffle=True, random_state=42) cv_rmse = [] for tr, val in kf.split(X_train): pls = PLSRegression(n_components=n_components) pls.fit(X_train[tr], y_train[tr]) pred = pls.predict(X_train[val]).ravel() cv_rmse.append(np.sqrt(mean_squared_error(y_train[val], pred))) # 用全部训练集建模,在独立测试集上评估 pls_final = PLSRegression(n_components=n_components) pls_final.fit(X_train, y_train) y_pred = pls_final.predict(X_test).ravel() test_rmse = np.sqrt(mean_squared_error(y_test, y_pred)) test_r2 = r2_score(y_test, y_pred) return { 'cv_rmse_mean': np.mean(cv_rmse), 'cv_rmse_std': np.std(cv_rmse), 'test_rmse': test_rmse, 'test_r2': test_r2, 'train_idx': train_idx, 'test_idx': test_idx }

这里n_components是 PLS 的主成分数,需要根据交叉验证的 RMSE 曲线来定,通常选 RMSE 最低且不再明显下降的那个点。test_ratio=0.3是验证集比例,小样本数据可以放到 0.2 到 0.3 之间。random_state=42只影响 KFold 的 shuffle,不影响 SPXY 的选样,因为 SPXY 本身是确定性的。返回的cv_rmse_std很关键,如果它比cv_rmse_mean还大,说明模型在不同折上表现差异明显,划分或模型复杂度可能有问题。

3.2 划分质量的量化检查

除了看模型指标,还可以直接检查划分本身的代表性。一个简单办法是计算训练集和验证集在 X 空间和 Y 空间的分布差异。常见做法是用 KS 检验对比两组的 Y 分布,或者用 PCA 降维后看两组样本在得分图上的重叠程度。如果验证集样本在 PCA 图上全部落在训练集的簇内,说明验证集没有提供新的信息,划分可能过于保守;如果验证集有大量样本落在训练集分布之外,说明 SPXY 选出的训练集覆盖不够,需要增加选样数量。

下面这段代码用 PCA 和 KS 检验做划分质量检查:

from sklearn.decomposition import PCA from scipy.stats import ks_2samp def check_split_quality(X, y, train_idx, test_idx): """ 检查 SPXY 划分的代表性 """ X_train, X_test = X[train_idx], X[test_idx] y_train, y_test = y[train_idx], y[test_idx] # PCA 降维看分布 pca = PCA(n_components=2) pca.fit(X) scores = pca.transform(X) train_scores = scores[train_idx] test_scores = scores[test_idx] # KS 检验对比 Y 分布 ks_stat, ks_p = ks_2samp(y_train, y_test) # 计算训练集和验证集在 PC1 上的均值差 pc1_diff = abs(train_scores[:, 0].mean() - test_scores[:, 0].mean()) return { 'ks_stat': ks_stat, 'ks_pvalue': ks_p, 'pc1_mean_diff': pc1_diff, 'train_pc1_range': (train_scores[:, 0].min(), train_scores[:, 0].max()), 'test_pc1_range': (test_scores[:, 0].min(), test_scores[:, 0].max()) }

ks_pvalue大于 0.05 说明两组 Y 分布没有显著差异,这是理想情况。pc1_mean_diff越小越好,如果超过训练集 PC1 范围的三分之一,就要警惕。这些检查不是必须的,但在小样本或 Y 分布偏斜时能提前发现问题,比等到模型评估翻车再回头查要省事。

4. 避坑与排查:SPXY 落地时最容易踩的五个坑

4.1 现象:选出的样本 Y 范围比全体窄很多

原因:SPXY 的距离是 X 和 Y 归一化后相加,如果 X 的维度远高于 Y,X 距离在联合距离里占主导,选样会偏向 X 空间分散而忽略 Y 的覆盖。解决:检查 X 和 Y 归一化后的距离贡献比例,必要时给 Y 距离加权重,比如dxy = dx_norm + alpha * dy_norm,alpha 取 1 到 5 之间试。或者先对 X 做 PCA 降维再算距离,减少维度差异的影响。

4.2 现象:样本量上万时内存爆掉或运行极慢

原因:cdist生成的是 n×n 稠密矩阵,n 大时内存和计算量都不可接受。解决:改用分块计算,每次只算一个候选块到已选集合的距离;或者用sklearn.neighbors.NearestNeighbors配合BallTree,只查最近邻不存全矩阵。另一个思路是先随机抽一个子集做 SPXY,再在子集上建模,但这样会损失代表性,只适合样本量极大的场景。

4.3 现象:SPXY 选出的训练集和验证集在 PCA 图上完全分开

原因:SPXY 倾向于选 X 空间边缘的样本,如果数据本身有聚集结构,选出的训练集可能集中在某些簇,验证集落在另一些簇。解决:这不是 SPXY 的 bug,而是数据分布问题。可以在 SPXY 之前先做聚类,每个簇内按比例选样,或者改用分层 SPXY:先按 Y 分箱,每个箱内做 SPXY。这样能保证训练集和验证集在各 Y 区间都有覆盖。

4.4 现象:交叉验证的 RMSE 波动很大,换一次划分结果差很多

原因:SPXY 是确定性的,但 KFold 的 shuffle 会引入随机性,如果样本量小,不同折之间的样本差异会被放大。解决:把shuffle设为 False,用连续的折,或者增加折数到 10 折,让每折样本更少但更均匀。更根本的办法是增加样本量,或者用重复交叉验证(Repeated KFold)取平均,降低单次划分的偶然性。

4.5 现象:SPXY 和 KS 选出的样本几乎一样

原因:当 Y 与 X 高度相关时,Y 距离提供的信息和 X 距离重叠,SPXY 退化成 KS。解决:这其实说明数据本身 X 对 Y 的解释性很强,不一定是坏事。如果确实需要 Y 空间的额外覆盖,可以检查 Y 是否有多峰或异常值,这些情况下 SPXY 才会和 KS 产生明显差异。如果数据没有这些特征,用 KS 也够,不必强上 SPXY。

5. 进阶技巧:用 SPXY 做模型集成与异常样本识别

SPXY 选出的样本顺序本身携带信息。第一个被选中的样本是距离最远的一对,最后被选中的是距离已选集合最近的样本。这个顺序可以用来做两件事:一是异常样本识别,排在最后几位的样本往往在 X 和 Y 空间都靠近已选集合,可能是重复或信息量低的样本,可以检查是否要剔除;二是模型集成,用 SPXY 选出的前 50%、70%、90% 样本分别建模,观察模型指标随样本量增加的变化曲线,判断当前样本量是否足够。

下面是一个用 SPXY 选样顺序做异常样本筛查的示例:

def spxy_outlier_screen(X, y, n_select, tail_ratio=0.05): """ 用 SPXY 选样顺序筛查尾部样本 tail_ratio: 尾部比例,默认 5% """ n = X.shape[0] selected = spxy(X, y, n_select) # 未被选中的样本 unselected = list(set(range(n)) - set(selected)) # 选样顺序中最后 tail_ratio 比例的样本 tail_n = max(1, int(n_select * tail_ratio)) tail_samples = selected[-tail_n:] # 计算尾部样本到已选集合的平均距离 dx = cdist(X, X, metric='euclidean') y2 = y.reshape(-1, 1) if y.ndim == 1 else y dy = cdist(y2, y2, metric='euclidean') dxy = dx / (dx.max() + 1e-12) + dy / (dy.max() + 1e-12) tail_dist = [] for s in tail_samples: others = [i for i in selected if i != s] tail_dist.append(np.min(dxy[s, others])) return { 'tail_samples': tail_samples, 'tail_min_dist': tail_dist, 'unselected_count': len(unselected) }

tail_ratio控制尾部比例,默认 5% 适合样本量几百到几千的场景。tail_min_dist越小,说明该样本越靠近已选集合,越可能是冗余样本。如果某个尾部样本的tail_min_dist接近零,同时它的 Y 值也在常见范围内,可以考虑剔除后重新建模对比。注意不要盲目剔除,有些样本虽然距离近但 Y 值特殊,剔除反而损失信息。我一般会先画尾部样本的 Y 分布,确认它们不是极端值再决定。

从那以后我每次做样本集划分,都会先跑一遍 SPXY,把选样顺序和距离矩阵存下来,再对比 KS 和随机划分的模型指标。如果 SPXY 和随机划分的验证 RMSE 差了两个百分点以上,说明数据里确实有信息泄漏,随机划分不可信。这个习惯帮我省了很多次回头返工的时间。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询