光谱回归建模中校正集与验证集划分:RS、KS、SPXY算法对比与Python实现 简介这份资源聚焦机器学习中校正集与验证集的划分方法面向本科、硕士及科研人员进行实验设计与教研学习帮助解决样本集合理拆分、模型评估可靠性等常见问题。包内包含SPXY算法、KS算法与RS算法三种典型划分策略的Matlab实现可直接用于光谱建模、回归预测等需要代表性样本选择的场景。资源共5个文件以4个m脚本文件和1个txt说明文件为主m文件承载各划分算法的核心代码txt则提示需自行带入数据压缩包整体约6KB轻量便于快速查阅与移植。目前已有332人学习下载读者可借此对比不同划分算法的实现思路与适用条件理解样本分布对建模结果的影响并在此基础上替换自有数据完成校正集与验证集的构建为后续模型训练与验证提供可复用的脚本参考。1. 校正集和验证集怎么分三个算法决定模型是骡子是马做回归建模的人多半遇到过这种玄学同一批光谱数据、同一个 PLSR 模型换个人重新划分校正集和验证集预测精度能从 R²0.95 掉到 0.72。这不是模型不行是划分方法在作祟。校正集calibration set用来训练模型验证集validation set用来评估模型泛化能力两者怎么切直接决定了你看到的精度是真实水平还是运气。随机划分 RS 简单粗暴但在样本分布不均时会把极端样本全塞进验证集KS 算法按光谱距离排序后等间隔抽样能保证校正集覆盖全浓度范围SPXY 算法同时考虑光谱距离和浓度距离是当前化学计量学里公认较稳的方案。这篇笔记把三种算法的原理、Python 实现、参数设置和踩坑记录一次讲透适合做近红外、拉曼、高光谱定量建模的从业者直接抄作业。2. RS、KS、SPXY 三种划分算法到底差在哪2.1 随机划分 RS最简单也最容易翻车RSRandom Sampling就是调numpy.random或sklearn的train_test_split按比例随机抽。它的假设是样本独立同分布但光谱数据往往存在浓度梯度随机抽可能导致校正集里高浓度样本扎堆、验证集里全是低浓度模型外推能力直接崩掉。我一般会先跑 RS 作为基线如果 RS 和 KS/SPXY 结果差距在 5% 以内说明样本分布本身比较均匀用 RS 省事差距大就说明数据有结构必须上 KS 或 SPXY。import numpy as np from sklearn.model_selection import train_test_split def rs_split(X, y, test_size0.3, seed42): X: 光谱矩阵 (n_samples, n_wavelengths) y: 浓度向量 (n_samples,) test_size: 验证集比例 seed: 随机种子固定后结果可复现 X_cal, X_val, y_cal, y_val train_test_split( X, y, test_sizetest_size, random_stateseed, shuffleTrue ) return X_cal, X_val, y_cal, y_val逻辑说明train_test_split默认 shuffleTruerandom_state必须固定否则每次跑结果都不一样论文里的精度没法复现。参数上test_size0.3是经验值样本量小于 100 时建议降到 0.2否则校正集样本太少模型欠拟合。2.2 KS 算法按光谱距离等间隔抽样KSKennard-Stone算法的核心思想是先选距离最远的两个样本进校正集然后每次选距离当前校正集最远的样本加入直到达到目标数量。这样校正集在光谱空间里分布最均匀。实现时用欧氏距离矩阵逐步更新最小距离。计算复杂度 O(n²)样本量上千时要注意内存。from scipy.spatial.distance import cdist def ks_split(X, y, n_cal): X: 光谱矩阵 y: 浓度向量 n_cal: 校正集样本数 n X.shape[0] D cdist(X, X, metriceuclidean) # 距离矩阵 cal_idx [] # 选距离最远的两个样本 i, j np.unravel_index(np.argmax(D), D.shape) cal_idx.extend([i, j]) # 剩余样本 remaining list(set(range(n)) - set(cal_idx)) while len(cal_idx) n_cal: # 每个剩余样本到校正集的最小距离 min_d [np.min(D[k, cal_idx]) for k in remaining] next_idx remaining[np.argmax(min_d)] cal_idx.append(next_idx) remaining.remove(next_idx) val_idx list(set(range(n)) - set(cal_idx)) return X[cal_idx], X[val_idx], y[cal_idx], y[val_idx]逻辑说明cdist算全距离矩阵np.argmax(D)找全局最远点对作为种子。循环里min_d是每个候选样本到已选集合的最近距离选最大的那个加入保证新样本离现有校正集最远。参数n_cal一般取总样本的 70%~80%样本量少于 50 时建议 80%。注意KS 只考虑光谱距离如果浓度分布不均匀可能出现校正集光谱覆盖好但浓度范围窄的情况这时候要上 SPXY。2.3 SPXY 算法光谱距离和浓度距离一起算SPXYSample set Partitioning based on joint X-Y distances在 KS 基础上做了改进距离度量同时包含光谱距离和浓度距离归一化后相加。这样选出的校正集在光谱空间和浓度空间都均匀。公式是 d_xy d_x/d_x_max d_y/d_y_max其中 d_x 是光谱欧氏距离d_y 是浓度欧氏距离。def spxy_split(X, y, n_cal): X: 光谱矩阵 y: 浓度向量 n_cal: 校正集样本数 # 光谱距离 Dx cdist(X, X, metriceuclidean) Dx Dx / Dx.max() # 浓度距离 y_2d y.reshape(-1, 1) Dy cdist(y_2d, y_2d, metriceuclidean) Dy Dy / Dy.max() # 联合距离 D Dx Dy n X.shape[0] cal_idx [] i, j np.unravel_index(np.argmax(D), D.shape) cal_idx.extend([i, j]) remaining list(set(range(n)) - set(cal_idx)) while len(cal_idx) n_cal: min_d [np.min(D[k, cal_idx]) for k in remaining] next_idx remaining[np.argmax(min_d)] cal_idx.append(next_idx) remaining.remove(next_idx) val_idx list(set(range(n)) - set(cal_idx)) return X[cal_idx], X[val_idx], y[cal_idx], y[val_idx]逻辑说明Dx和Dy分别归一化到 [0,1] 再加权避免量纲差异导致某一项主导。Dx.max()和Dy.max()是归一化因子必须用当前数据集的最大值不能写死。参数n_cal和 KS 一致但 SPXY 对浓度梯度大的数据集更稳。提示SPXY 计算量比 KS 大样本超过 2000 时建议先做 PCA 降维再算距离否则cdist会吃满内存。3. 用 Python 把三种划分跑通从数据到精度对比3.1 数据准备和预处理拿一份近红外光谱数据举例假设X是 200×500 的矩阵y是 200 个浓度值。预处理先做 SNV标准正态变量变换消除散射影响再做一阶导数增强峰特征。import numpy as np from scipy.signal import savgol_filter def snv(X): 标准正态变量变换 mean X.mean(axis1, keepdimsTrue) std X.std(axis1, keepdimsTrue) return (X - mean) / std def first_derivative(X, window11, poly2): Savitzky-Golay 一阶导数 return savgol_filter(X, window_lengthwindow, polyorderpoly, deriv1, axis1) # 假设 X_raw, y 已加载 X snv(X_raw) X first_derivative(X)逻辑说明snv按行做标准化消除样本间基线漂移。savgol_filter的window_length必须奇数polyorder小于window_length一阶导数用deriv1。参数window11是经验值光谱分辨率高时可以调到 15 或 21。3.2 三种划分的完整调用和建模用 PLSR偏最小二乘回归做模型统一用 10 折交叉验证选主成分数然后看验证集 RMSEP 和 R²。from sklearn.cross_decomposition import PLSRegression from sklearn.metrics import mean_squared_error, r2_score def evaluate_split(X_cal, X_val, y_cal, y_val, n_components10): pls PLSRegression(n_componentsn_components) pls.fit(X_cal, y_cal) y_pred pls.predict(X_val).ravel() rmsep np.sqrt(mean_squared_error(y_val, y_pred)) r2 r2_score(y_val, y_pred) return rmsep, r2 # RS X_cal_rs, X_val_rs, y_cal_rs, y_val_rs rs_split(X, y, test_size0.3, seed42) rmsep_rs, r2_rs evaluate_split(X_cal_rs, X_val_rs, y_cal_rs, y_val_rs) # KS X_cal_ks, X_val_ks, y_cal_ks, y_val_ks ks_split(X, y, n_cal140) rmsep_ks, r2_ks evaluate_split(X_cal_ks, X_val_ks, y_cal_ks, y_val_ks) # SPXY X_cal_spxy, X_val_spxy, y_cal_spxy, y_val_spxy spxy_split(X, y, n_cal140) rmsep_spxy, r2_spxy evaluate_split(X_cal_spxy, X_val_spxy, y_cal_spxy, y_val_spxy) print(fRS: RMSEP{rmsep_rs:.4f}, R2{r2_rs:.4f}) print(fKS: RMSEP{rmsep_ks:.4f}, R2{r2_ks:.4f}) print(fSPXY: RMSEP{rmsep_spxy:.4f}, R2{r2_spxy:.4f})逻辑说明PLSRegression的n_components是主成分数一般通过交叉验证确定这里固定 10 方便对比。evaluate_split返回 RMSEP 和 R²RMSEP 越小、R² 越接近 1 越好。实际跑下来SPXY 通常比 RS 的 RMSEP 低 10%~30%KS 介于两者之间。3.3 参数怎么调校正集比例和主成分数校正集比例不是固定的。样本量 50~100 时校正集占 80%100~500 时70%~75%超过 500 时60%~70% 就够。主成分数用交叉验证的 RMSECV 最小值确定不要超过校正集样本数的 1/10。样本量校正集比例主成分数上限5080%550-10075%-80%8100-50070%-75%1550060%-70%20注意主成分数过多会过拟合RMSEC 很低但 RMSEP 很高这时候要回头检查划分方法是不是把异常样本漏进校正集了。4. 避坑记录划分校正集和验证集时最容易翻车的 5 个点4.1 现象RS 每次跑精度都不一样论文没法复现原因train_test_split没固定random_state或者用了shuffleTrue但没设种子。解决所有随机操作必须固定种子seed42是惯例写进代码注释里。4.2 现象KS 选出的校正集浓度范围比验证集窄原因KS 只按光谱距离选样本如果浓度和光谱不是强相关会出现光谱覆盖好但浓度覆盖差的情况。解决换 SPXY或者先按浓度分层再做 KS。4.3 现象SPXY 跑得特别慢内存爆了原因cdist算的是 n×n 全距离矩阵样本 5000 时矩阵是 5000×5000float64 占 200MB加上中间变量直接吃满。解决先做 PCA 降到 20~50 维再算距离或者用scipy.spatial.distance.pdist算压缩距离。4.4 现象校正集 R²0.99验证集 R²0.6原因校正集里混入了异常样本或者校正集和验证集有重叠。解决建模前用马氏距离或 PCA 的 Hotelling T² 筛异常样本划分后检查两个集合的索引有没有交集。4.5 现象换了批次数据SPXY 效果还不如 RS原因不同批次的光谱基线差异大直接算欧氏距离会把批次差异当成样本差异。解决先做批次校正如 PDS、OSC再跑 SPXY。如果批次效应太强考虑按批次分层划分。5. 进阶技巧用 Kennard-Stone 的变体做分层 SPXY标准 SPXY 有个隐患如果浓度分布本身不均匀比如低浓度样本占 80%联合距离会被浓度项主导选出的校正集在光谱空间反而稀疏。我一般会做分层 SPXY先按浓度分箱每个箱内按比例抽样本再在抽出的样本上跑 SPXY。def stratified_spxy_split(X, y, n_cal, n_bins5): 分层 SPXY先按浓度分箱每箱按比例抽候选样本再跑 SPXY bins np.linspace(y.min(), y.max(), n_bins 1) bin_idx np.digitize(y, bins) - 1 bin_idx np.clip(bin_idx, 0, n_bins - 1) candidate_idx [] for b in range(n_bins): idx_b np.where(bin_idx b)[0] n_b max(2, int(len(idx_b) * n_cal / len(y))) n_b min(n_b, len(idx_b)) chosen np.random.choice(idx_b, n_b, replaceFalse) candidate_idx.extend(chosen) candidate_idx np.array(candidate_idx) X_sub, y_sub X[candidate_idx], y[candidate_idx] X_cal_sub, X_val_sub, y_cal_sub, y_val_sub spxy_split(X_sub, y_sub, n_calmin(n_cal, len(candidate_idx))) # 映射回原始索引 return X_cal_sub, X_val_sub, y_cal_sub, y_val_sub逻辑说明np.digitize按浓度分箱每个箱按比例抽候选样本保证低浓度和高浓度都有代表。n_b max(2, ...)保证每箱至少 2 个样本否则 SPXY 选不出距离最远点对。参数n_bins5是经验值浓度范围跨两个数量级时用 8~10 箱。验证方法跑 10 次不同随机种子的分层 SPXY看 RMSEP 的均值和标准差。如果标准差小于 0.05说明划分稳定如果大于 0.1说明数据本身有问题先查异常样本和批次效应。我自己的习惯是拿到新数据先跑 RS 看基线再跑 KS 和 SPXY 对比如果 SPXY 比 RS 的 RMSEP 低不到 5%说明数据分布均匀直接用 RS 省时间如果低 15% 以上必须用 SPXY并且把划分代码和随机种子一起存档不然三个月后回头复现就是一场血泪史。希望帮到你。本文还有配套的精品资源点击获取