简介面向遥感、农业与环境监测中的高光谱图像分类需求一套基于 MATLAB 的轻量程序集提供了偏最小二乘法PLS、支持向量机SVM与 BP 网络三种分类器的完整实现可供相关领域的研究生、工程师快速上手。压缩包共包含 22 个文件其中 6 个 .m 算法脚本、14 个 .mat 数据文件、2 个 .asv 自动保存文件整体大小仅 100KB便于快速下载与学习。其中 .m 脚本涵盖主程序与核心分类函数.mat 文件存放训练集、测试集及标签数据运行主程序即可完成数据加载、降维、模型训练与分类结果输出帮助快速对比算法效果。目前已有 213 人学习下载适合具备一定 MATLAB 基础、希望系统掌握高光谱数据降维与分类流程的初学者。通过比较 PLS、SVM 与 BP 的分类结果读者能够理解特征提取与分类器结合的思路并将这套程序迁移到自己的遥感或光谱数据处理任务中。1. 高光谱分类走到分类器这一步偏最小二乘、SVM 与 BP 三种路线的 Matlab 实现高光谱数据一来就是几百个波段样本却往往只有几十个直接拿原始光谱去跑分类器十有八九要走弯路。高光谱 分类这四个字背后真正卡人的不是算法原理而是维度灾难和小样本之间的拉扯。这份资源把三条成熟路线直接打包偏最小二乘法PLS先降维再分类、支持向量机SVM吃光谱特征、BP 神经网络端到端训练全部以 Matlab 源码和 mat 数据文件形式给出。适合刚把高光谱数据读进工作区、正在纠结选哪条路线的人也适合想快速做三类方法精度对比的从业者。2. PLS 降维分类从 pls.m 到 main_DPLS.m 的完整主成分流程2.1 先看清压缩包里的数据文件在表达什么拿到压缩包别急着跑 main_DPLS.m先把 mat 文件分门别类归拢一遍。第一类是成对出现的训练集和测试集dataTrain.mat 和 dataTest.mat。从命名习惯看dataTrain 里应是一个二维光谱矩阵行是样本、列是波段也可能额外含一列类别标签。高光谱数据的标准组织方式就是「样本 × 波段」把空间像素展平成行所以矩阵宽度通常等于波段数几十到几百列不等。第二类是标签文件Y.mat。注意 Y 和后面 BP 路线里的 label.mat 是两个文件别混用。Y.mat 大概率是 PLS 和 SVM 共用的标签label.mat 是 BP 专用的标签两个文件的行数必须和各自对应数据的样本数严格一致。第三类是带数字后缀的独立光谱文件a3.mat、a10.mat、d10.mat、d15.mat、d20.mat、d25.mat、d30.mat。这些数字极有可能代表主成分个数或波段分组编号。比如 d15.mat 大概率是 PLS 取 15 个主成分时保存的中间结果。你可以 load 后看一眼变量维度行数等于样本数、列数等于 15基本就能验证这个猜测。一个容易忽略的细节main_DPLS.asv 和 SVM.asv 是 Matlab 自动保存的备份文件。这两个不是可执行脚本内容可能是上一次手动保存前的旧版本。有人双击 asv 文件运行结果 Matlab 报「未定义函数或变量」折腾半天才发现是文件类型不对。我拿到压缩包的第一件事就是按扩展名把 asv 文件捡出来统一改名归档避免误用也避免混淆版本。2.2 pls.m 内部到底在迭代什么pls.m 是偏最小二乘法的核心函数。PLS 和主成分分析 PCA 最大的区别是PCA 只对光谱矩阵 X 做方差最大化完全不看标签而 PLS 在提取主成分时同时盯着 X 和标签 Y找到的方向既要能解释 X 的方差又要与 Y 有最大协方差。这在高光谱场景里很重要因为原始光谱里方差最大的方向可能只是背景或噪声的贡献和地物类别没有关系。一个典型的 pls.m 函数会长成下面这样function [T, W, P, B, Q] pls_core(X, Y, ncomp) % X: 光谱矩阵, 行样本, 列波段 % Y: 标签/响应矩阵, 行样本, 列响应变量个数 % ncomp: 需要提取的主成分个数 [n, p] size(X); % n 样本数, p 波段数 m size(Y, 2); % 每个特征做 z-score 标准化 X0 (X - mean(X)) ./ std(X); Y0 (Y - mean(Y)) ./ std(Y); T zeros(n, ncomp); % 得分矩阵, 每列一个主成分 P zeros(p, ncomp); % X 的载荷矩阵 W zeros(p, ncomp); % 权重矩阵 Q zeros(m, ncomp); % Y 的载荷矩阵 B zeros(p, m); % 回归系数矩阵 Xk X0; Yk Y0; for k 1:ncomp % 1. 权重向量: X*Y 的归一化方向 w (Xk * Yk) / norm(Xk * Yk); W(:, k) w; % 2. 得分: 样本在权重方向上的投影 t Xk * w; T(:, k) t; % 3. 载荷: 用得分对 X 和 Y 做回归 p Xk * t / (t * t); q Yk * t / (t * t); P(:, k) p; Q(:, k) q; % 4. 残差更新: 抹掉当前主成分已经解释的部分 Xk Xk - t * p; Yk Yk - t * q; % 5. 累加回归系数 B B (w * q) / (w * p); end end这段代码把 PLS 的核心迭代拆成五步求权重、算得分、求载荷、更新残差、累加回归系数。权重 w 的物理含义是每个波段对标签贡献的方向矢量w 里绝对值大的位置对应重要波段。得分 t 是样本在 w 方向上的投影值相当于把每个样本从原始波段空间压到一个数ncomp 个主成分叠加起来就得到降维后的得分矩阵 T。残差更新是 PLS 最不一样的地方每提取一个主成分就把它解释掉的信息从 X 和 Y 里同时扣除下一轮找到的新方向自然与前面成分正交。参数 ncomp 是最需要手动调的数。取太小重要光谱差异没被充分提取取太大后面的成分主要在拟合噪声和个别样本。一个实用的做法是在 5 到 30 之间按步长 5 扫一遍看验证集精度的拐点。d15.mat 存在大概率就是因为作者发现 15 个主成分在这份数据上表现比较均衡。2.3 main_DPLS.m 的完整链路读数据、提成分、分类、评估main_DPLS.m 是 PLS 路线的主程序把上面这些散件串成一条流水线%% main_DPLS.m % Step1: 载入数据 load(dataTrain.mat); % 期望存在 X_train load(dataTest.mat); % 期望存在 X_test load(Y.mat); % 期望存在 label_train % Step2: PLS 提取主成分 ncomp 15; [T_train, W, P, ~, ~] pls_core(X_train, label_train, ncomp); % Step3: 把测试集投影到训练集的主成分空间 T_test (X_test - mean(X_train)) * W * pinv(P * W); % Step4: 在得分矩阵上训练分类器 model fitcdiscr(T_train, label_train); pred predict(model, T_test); % Step5: 评估精度 acc sum(pred label_test) / length(label_test); fprintf(PLS QDA 测试集精度: %.4f\n, acc);这份主程序里有三个细节必须拎出来说透。第一个是测试集投影。T_test 必须用训练集上得到的 W 和 P 去计算不能重新在测试集上跑一遍 pls_core。原因在于 PLS 提取的是训练数据中最具判别力的方向测试集的投影方向一旦重算两个主成分空间就失去可比性分类器面对的是一个错位的特征空间。更隐蔽的是重算意味着测试集信息已经参与了主成分方向的确定相当于把测试集内容透露给模型精度会虚高。这类数据泄漏没有后悔药只能重新组织流程。第二个是伪逆 pinv 的选择。P*W 在波段数远大于样本数的场景下经常是病态矩阵直接 inv 会报「矩阵接近奇异」的警告或直接崩溃。pinv 通过奇异值分解求伪逆即使矩阵秩亏也能给出一组最小范数解稳定性好得多。如果发现某个 ncomp 下 pinv 结果异常先降低 ncomp 再看。第三个是分类器的选择。主程序里我用了二次判别分析 QDA但这只是选项之一。压缩包同时给了 SVM.m说明作者很可能在 PLS 降维之后接的是 SVM你完全可以把 Step4 换成 fitcecoc(T_train, label_train, Learners, templateSVM(KernelFunction,linear))这就是经典的「PLS 降维 SVM 分类」组合。跑完主程序后把 ncomp 改成 5、10、20、25、30 分别跑一遍记录精度变化。你会看到 5 个主成分精度偏低15 到 20 左右达到峰值再往后开始缓慢下降。这条曲线本身就是回答「为什么压缩包里会有 d15.mat 和 d25.mat 这种不同数字文件」的最好证据。参考参数表现如下ncomp典型现象建议5精度偏低判别信息不足测试下限15精度接近峰值默认起点2530精度下降或过拟合迹象上限参考3. SVM 分类路线SVM.m 与自定义核函数的形式.m 怎么配合3.1 为什么高光谱场景常选 SVM 而不是线性分类器高光谱数据的光谱曲线是噪声、基线漂移和多种物质吸收特征叠加的结果类别边界在原始波段空间里经常不是线性可分的。SVM 通过核函数把样本隐式映射到高维特征空间在间隔最大化的意义下找决策边界对「样本少、特征多」的高光谱数据有天然优势。也正因为特征维度极高SVM 对过拟合的控制能力比很多传统分类器更稳。但这个优势是有前提的。假设你有 200 个训练样本、400 个波段直接拿 RBF 核在原始空间训练核矩阵规模是 200×200 还好说但 400 维里真正有用的判别信息可能只集中在少数波段大量无关波段会稀释核函数对样本相似度的判断精度反而上不去。所以高光谱 SVM 的常见做法是先降维或做波段选择再进分类器。这也解释了压缩包的结构既有纯 SVM 路线也有 PLS 与 SVM 的组合路线。3.2 自定义核函数的形式.m 到底在写什么这个文件是压缩包里名字最特殊的一个。Matlab 的 fitcsvm 支持通过 KernelFunction 参数传入自定义核签名固定为 function K kernel(U,V)要求返回 Gram 矩阵。一个典型的自定义核长这样function K custom_kernel(U, V) % U: 训练或预测用的样本矩阵 % V: 支持向量或另一组样本 % 返回 K(i,j) kernel(U(i,:), V(j,:)) % 在双循环里实现多项式核 p 3; K zeros(size(U, 1), size(V, 1)); for i 1:size(U, 1) for j 1:size(V, 1) K(i, j) (dot(U(i, :), V(j, :)) 1)^p; end end end双循环版本直观但慢。样本规模上千时这个双循环会占到整个训练时间的 90% 以上。改成矩阵运算版后结果完全一致速度则能快一个数量级function K custom_kernel_fast(U, V) % 矩阵化实现等价于上面的双循环版本 p 3; G U * V; % 一次矩阵乘法得到所有内积 K (G 1).^p; % 逐元素做多项式映射 end自定义核函数有几个容易翻车的细节。首先是函数签名fitcsvm 在不同阶段调用核函数时传入的 U 和 V 行数不一定相等比如预测阶段可能传一个样本和整个支持向量集函数内部不要对输入尺寸做任何固定假设。其次是核矩阵的性质自定义核必须保证 Gram 矩阵半正定否则求解器会报错或给出完全没意义的分类结果。再次p、theta 这类核超参数fitcsvm 外层框架不会帮你调得自己在外面套循环做网格搜索。如果你打开「自定义核函数的形式.m」发现是纯双循环版本别意外那很可能是作者最初验证算法正确性的写法。你在使用时直接在上面改成矩阵化版本即可。3.3 SVM.m 的多分类链路fitcecoc 与模板设置SVM.m 是这条路线的主程序。高光谱分类往往是多类问题但 fitcsvm 原生只做二分类所以主程序里通常会使用 fitcecoc 做一对其余扩展%% SVM.m load(dataTrain.mat); load(dataTest.mat); load(Y.mat); % 训练集内部切分 15% 作为验证 rng(42); idx randperm(size(X_train, 1)); val_num round(0.15 * length(idx)); val_idx idx(1:val_num); train_idx idx(val_num1:end); X_tr X_train(train_idx, :); Y_tr label_train(train_idx, :); X_va X_train(val_idx, :); Y_va label_train(val_idx, :); % 用线性核作为基线 t templateSVM(KernelFunction, linear, BoxConstraint, 1, ... Standardize, true); % 一对多编码分多类 mdl fitcecoc(X_tr, Y_tr, Learners, t); % 验证集精度 pred predict(mdl, X_va); va_acc mean(pred Y_va); fprintf(SVM(linear) 验证集精度: %.4f\n, va_acc);想换成压缩包里的自定义核只需要把 templateSVM 的 KernelFunction 改成函数句柄t templateSVM(KernelFunction, custom_kernel_fast, ... BoxConstraint, 1, Standardize, true);这里 BoxConstraint 就是 SVM 的 C 参数。C 太小间隔大但误分类多C 太大模型会被个别样本带着走。高光谱小样本场景我一般从 C1 开始用 0.1、1、10、100 网格扫一遍看验证集表现。Standardize 建议保持 true因为不同波段的响应值可能差几个数量级不标准化的话核函数里的内积会被量级大的波段主导。a10.mat、a3.mat 这些文件如果维度是「样本数 × 10」或「样本数 × 3」说明这条 SVM 路线很可能吃的是降维后的特征。你在 main 里相应换成 load(a10.mat) 就能复现「PLS 降到 10 维再进 SVM」的实验设置。4. BP 神经网络路线Bp.m 的数据组织与训练参数4.1 BP 在高光谱分类里的定位与数据组织BP 神经网络在这份资源里代表完全不同的路径不显式降维让网络自己学特征组合。当样本量相对充足比如每类几百个BP 往往能捕捉到 PLS 和 SVM 抓不住的非线性映射。但高光谱场景样本通常不多BP 也容易过拟合所以这条路线的真实定位是「调参上限高、但最考验折腾能力」。Bp.m 对应的数据是 data1.mat、data2.mat、data3.mat 和 label.mat。三个 data 文件很可能是三种不同的光谱表示比如原始反射率、归一化反射率、波段平均后的降采样特征。动手前先确认维度d1 load(data1.mat); d2 load(data2.mat); d3 load(data3.mat); lb load(label.mat); % 逐个查看变量名和大小 disp(fieldnames(d1)); disp(size(d1));如果 data1 是 100×200 的二维矩阵而 label 是 100×1说明每行是一个样本的光谱、每列是一个波段组织结构正确。如果 data 是三维数组需要先用 reshape 把它展平成二维公式是 reshape(data, size(data,1)*size(data,2), size(data,3))但要注意展平顺序先按行取还是先按列取会影响样本对应关系。4.2 Bp.m 训练流程与网络参数说明一个典型的 Bp.m 用 feedforwardnet 搭两层隐藏层模型%% Bp.m load(data1.mat); % X: 样本×特征 load(label.mat); % Y: 样本×类别整数标签 % 整数标签转 one-hot classes unique(Y); Y_onehot zeros(size(Y, 1), length(classes)); for i 1:length(classes) Y_onehot(Y classes(i), i) 1; end % 数据集划分 rng(2024); idx randperm(size(X, 1)); train_num floor(0.8 * size(X, 1)); X_train X(idx(1:train_num), :); Y_train Y_onehot(idx(1:train_num), :); X_test X(idx(train_num1:end), :); Y_test Y_onehot(idx(train_num1:end), :); % 网络结构: 两个隐藏层 net feedforwardnet([20, 10]); net.trainFcn trainlm; net.trainParam.epochs 500; net.trainParam.goal 1e-4; % 在训练集上计算标准化参数 mu mean(X_train); sigma std(X_train); X_train_norm (X_train - mu) ./ (sigma 1e-8); X_test_norm (X_test - mu) ./ (sigma 1e-8); % 训练与预测 net train(net, X_train_norm, Y_train); pred_raw net(X_test_norm); [~, pred] max(pred_raw, [], 2); % 还原真实类别标签 pred_label classes(pred); acc mean(pred_label Y(idx(train_num1:end), :)); fprintf(BP 测试集精度: %.4f\n, acc);feedforwardnet([20,10]) 表示两层隐藏层第一层 20 个神经元、第二层 10 个。高光谱特征如果有几百维20 个输入端很可能不够可以按特征数的一半起步。trainlm 是 Levenberg-Marquardt小样本收敛快但内存占用大如果数据上万行换成 trainscg 更稳妥。标准化里加 1e-8 小常量是防止某个波段标准差恰好为零。高光谱里相邻波段经常完全相关标准化后除零会产生 NaN这个细节能避免很多玄学错误。one-hot 转回去也藏着一个坑max 直接取最大下标但如果输出层有两个节点同时逼近 0.5模型不会报错只是置信度很低。更稳的做法是同时看最大和第二大输出值的差差值小于阈值就标为不确定样本留给人工作进一步判断。Bp.m 里 data2、data3 的存在意味着你可以直接换数据文件做对比。把 load(data1.mat) 改成 load(data2.mat)其余流程不动精度差异会直接告诉你哪种预处理对这个数据集更友好。5. 三种方法实测避坑维度、归一化与核函数失效的真实记录5.1 现象load 后变量名对不上size 和预期不一致现象运行 main_DPLS.m 报「未识别的变量 X_train」或「矩阵维度必须一致」。原因dataTrain.mat 里存的变量名不叫 X_train可能是 data、X 或者 spectral直接用 load(dataTrain.mat) 把所有变量丢进工作区但你的脚本按固定变量名引用自然对不上。解决先看变量再取值不要盲猜变量名S load(dataTrain.mat); disp(fieldnames(S)); % 列出里面所有变量名 X_train S.data; % 按实际字段名取这个习惯在数据来源混杂时特别重要不同人保存 mat 文件的命名风格差异极大不看变量名直接 load 就是在赌。5.2 现象PLS 调用 inv 报「矩阵接近奇异值」现象main_DPLS.m 跑到 T_test X_test * W * inv(P*W) 时Matlab 弹出警告或直接报错崩溃。原因P*W 是方阵但当波段数 p 大于样本数 n 时这个矩阵必然秩亏、不可逆。高光谱的典型情况就是几百波段对几十样本在 PLS 里几乎必然撞上。解决把 inv 换成 pinv。pinv 基于奇异值分解求广义逆矩阵秩亏时也能给出最小二乘意义上的解。同时把 ncomp 调小一点比如从 15 降到 10也会显著降低病态程度。5.3 现象SVM 预测全部落到同一类别现象fitcsvm 训练时没报错但验证集预测结果全是一个标签精度刚好等于最大类样本占比。原因fitcsvm 是纯二分类器。高光谱多类场景直接喂三分类标签相当于强行把所有样本二分成两类某个类很容易被完全放弃预测结果自然全部落到另一类上。解决改用 fitcecoc通过一对一或一对其余策略组合多个二分类器t templateSVM(KernelFunction, linear, Standardize, true); mdl fitcecoc(X_tr, Y_tr, Learners, t);这一个改动通常就能把多类精度拉回正常水平。5.4 现象BP 的 loss 卡在 1e-3精度在 70% 左右上不去现象train 输出显示均方误差收敛到 10^-3 量级就停住测试集精度一直在 70% 附近波动。原因隐藏层神经元数量太少模型容量不够或者输入标准化后仍有样本落在激活函数饱和区梯度接近于零网络学不动。解决先把隐藏层从 [20,10] 放宽到 [30,15]看收敛误差是否继续下降。再把标准化方式从 z-score 换成 min-maxX_min min(X_train); X_max max(X_train); X_train_norm (X_train - X_min) ./ (X_max - X_min 1e-8); X_test_norm (X_test - X_min) ./ (X_max - X_min 1e-8);把输入压到 [0,1]tanh 或 sigmoid 才能工作在敏感区。注意 min 和 max 仍然只能在训练集上计算这条规矩和 PLS 投影是同一个原则。5.5 现象asv 文件当脚本运行报错现象双击 main_DPLS.asv 或在命令行直接输入 main_DPLS.asv编辑器提示「未定义函数或变量」。原因asv 是 Matlab 自动保存的备份文件不是正式可执行脚本。解决先重命名为 main_DPLS_backup.m再打开看内容。如果备份内容比当前 .m 版本新说明上次编辑时触发了自动保存但没手动存档把备份转正反而能找回一部分工作。6. 验证与进阶把固定数据集换成你自己的高光谱数据6.1 三分钟跑通验证流程解压后先不动任何参数按顺序跑 main_DPLS.m、SVM.m、Bp.m确认每个脚本能完整输出一个精度数字。这一步的意义是确认当前 Matlab 版本和工具箱满足代码依赖。如果某个脚本报错优先检查是不是缺统计机器学习工具箱或神经网络工具箱。跑通后把 ncomp 和 BoxConstraint 各自扫一遍记录精度变化形成第一条属于自己的实验基线。6.2 换成自己的数据把自己数据的格式整理成「每行一个样本、每列一个波段」的二维矩阵标签整理成「样本数 × 1」的列向量分别存为 mydata.mat 和 mylabel.mat。然后把三个主程序里的 load 语句对应替换load(mydata.mat); load(mylabel.mat);X_train、label_train 这类变量名改成你实际保存的变量名其余逻辑可以复用。如果你的数据类别数超过三类fitcecoc 会自动扩展编码Bp.m 里的 one-hot 循环也会自动按 classes 动态生成不需要额外改结构。只有 PLS 的 ncomp 需要重新扫一遍因为新数据的光谱冗余程度和原来那份数据集未必一样。6.3 先看维度再谈精度最后说一个习惯。每次换新数据我都强制自己走一遍「变量维度检查 → 训练测试投影一致 → 类别数确认」三步。第一次在 PLS 投影上翻车之后我再也没有跳过维度检查每次都是打印 size 之后才敢开始训练。这个习惯帮我挡掉了至少十次数据泄漏和维度错位。希望帮到你。本文还有配套的精品资源点击获取