
简介面向高光谱遥感图像分类任务这套Matlab实现选用Pavia University公开数据集系统展示SVM、CNN与KNN在同一实验中的建模与比较过程适用于遥感地物识别方向的研究生、工程师及算法学习者。包体共35个文件包含27个.m源码脚本、7个.mat格式的数据与预训练网络文件、1份txt程序说明压缩包大小35.42MB源码覆盖原始数据读取、归一化与PCA/KPCA/LDA降维CNN网络搭建、训练与测试以及SVM和KNN分类器的调用与性能评估。目前已有758人学习下载。通过该资源读者可直接在Matlab中复现高光谱分类实验对比核方法、卷积特征提取与近邻投票的精度差异并基于现成脚本调整参数或更换特征适合需要快速上手多算法高光谱分类实践的初中级学习者。1. 把 SVM / CNN / KNN 放到同一份 PaviaU 数据上这份 Matlab 包到底能跑出什么做高光谱分类的人应该都有过这种经历想对比经典机器学习和深度学习在同一份数据上的表现结果光是对齐数据格式就花了一整天。PaviaU 是 103 个波段的高光谱影像直接丢给 SVM 或者 KNN特征维数高到离谱而 CNN 又需要你把数据重新组织成图像块。这份 Matlab 资源把三条路线全打通了预处理脚本、三个分类器的训练与测试代码、还有已经训练好的 CNN 网络都在一个压缩包里解压之后理论上可以直接跑通完整流程。它适合两类人一是遥感课程作业或者本科毕设需要快速出分类对比图的学生二是想拿 Matlab 复现基准实验、又不想从零写 DeepLearnToolbox 接口的从业者。我拆完这套代码之后最大的感受是真正的价值不在三个分类器本身而在它把数据预处理、降维、划分训练测试集这一整套脏活提前做完了。2. 从 PaviaU.mat 到 train_x.mat预处理与降维管线详解2.1 原始数据到底长什么样PaviaU 全称是 Pavia University是由 ROSIS 传感器获取的高光谱遥感影像空间分辨率 1.3 米原始图像尺寸是 610×340 像素波段数是 103。这份资源里的 PaviaU.mat 是已经裁剪和去噪后的版本和 PaviaU_gt.mat 的地物真值标签一一对应类别一共 9 类包括沥青、草地、树木、金属板、裸土等典型地物。之所以不用整幅影像直接训练是因为高光谱相邻波段之间相关性极强103 个波段直接作为特征输入不仅计算量大还会因为维数灾难让 SVM 和 KNN 的表现严重下滑。这套代码里 data_undone.m 就是原始数据入口脚本。它做的工作很基础但很关键从 PaviaU.mat 中按真值图的位置取出有标签的像素点把光谱向量和标签一一对应起来。PaviaU 上真正有标注的像素大约 42776 个其中训练集和测试集的比例在 main_final.m 里控制。% data_undone.m 核心逻辑 load(PaviaU.mat); load(PaviaU_gt.mat); [rows, cols, bands] size(PaviaU); % 找出有标签的像素索引 idx find(PaviaU_gt 0); % 逐像素提取光谱向量 data zeros(length(idx), bands); for i 1:length(idx) [r, c] ind2sub([rows, cols], idx(i)); data(i, :) squeeze(PaviaU(r, c, :)); end labels PaviaU_gt(idx);这段代码的逻辑在于把三维影像张量展平成二维样本矩阵每一行是一个像素的光谱响应列数固定为 103。注意squeeze是为了把 1×1×103 的三维切片压成一维向量否则直接赋值会报维度错误。用find(PaviaU_gt 0)是因为背景像素的真值标记为 0这些点没有类别含义不能参与训练。这里还有个容易被忽略的点PaviaU 原始数据里的灰度值是 0 到 65535 的 16 位整数如果直接拿去算欧氏距离或者喂给 CNN数值范围太大会让梯度计算出现问题所以后续脚本里必然有归一化处理。2.2 三条降维路线PCA、KPCA、LDA 各自解决什么问题这份资源在降维上给了三个脚本data_pca_done.m、data_lda_done.m 和 kpcaFordata.m。这不是冗余设计而是三条有针对性的路线。PCA 是无监督降维它不关心类别标签只找方差最大的投影方向适合在不知道类别信息的情况下先压缩数据。KPCA 是 PCA 的核方法版本它通过核函数把数据映射到高维空间再做 PCA能处理线性不可分的数据结构。LDA 则是监督降维它显式利用标签信息目标是让类间散度最大、类内散度最小因此在小样本高光谱分类中往往效果比 PCA 好。% data_pca_done.m 关键片段 - 带归一化的 PCA load(data_undone.mat); % 假设已经用 data_undone.m 生成了 % 归一化零均值单位方差 mu mean(data); sigma std(data); sigma(sigma 0) 1; % 避免除零 data_norm (data - mu) ./ sigma; % 计算协方差矩阵 covMat cov(data_norm); % 取前 30 个主成分 [V, ~] eigs(covMat, 30); pca_data data_norm * V;这里eigs只计算前 30 个特征向量比直接用eig求全部特征向量快得多PaviaU 的样本量在四万级别协方差矩阵是 103×103全量特征分解其实不算慢但养成用eigs的习惯在大数据量下能省很多内存。sigma(sigma 0) 1这行是典型的防御性编程某些波段如果全是常数标准差就是 0直接归一化会出现 NaN。LDA 在 Matlab 里实现时有个更隐蔽的坑后面避坑章节再展开。我一般在实际项目中会把三条降维管线都跑一遍然后把降维后的数据分别喂给同一个分类器对比精度用结果反推哪条管线更适合当前数据分布。这个资源的设计思路也正是这样data_pca_done.m、data_lda_done.m 对应的是 PCA 和 LDA 的输出kpca_test.m 则把 KPCA 的测试流程单独拆出来了。3. SVM 与 KNN 分类实践从参数配置到精度对比3.1 用 fitcecoc 实现多分类 SVM核函数与 One-vs-One 策略SVM 原生是二分类器PaviaU 有 9 类地物需要用多分类策略扩展。Matlab 的统计工具箱里fitcecoc封装了两种策略One-vs-One 和 One-vs-All。高光谱分类场景下一般选 One-vs-One因为 PaviaU 的类别之间存在明显的谱间相似性比如两种植被类型的可分性较差OvO 策略每个分类器只面对两个类别决策边界更精细。% ZKL_classifier_final.m 中 SVM 训练核心代码 % 读取 PCA 预处理后的训练集 load(data_pca_done.mat); load(train_y.mat); % 划分训练测试集这里只截取核心训练部分 t templateSVM(KernelFunction, rbf, ... BoxConstraint, 1, ... KernelScale, auto, ... Standardize, false); svm_model fitcecoc(train_x, train_y, ... Learners, t, ... Coding, onevsone, ... Verbose, 1); % 测试 [pred_label, ~] predict(svm_model, test_x); accuracy sum(pred_label test_y) / length(test_y);KernelScale设为 auto 是让 Matlab 自动用启发式算法估计 RBF 核的尺度参数避免手动调 gamma。BoxConstraint对应的是软间隔 SVM 的惩罚系数 C默认值 1 在 PaviaU 上效果一般我把 C 调到 10 之后精度大约能涨两个百分点。但请注意这只是在当前样本划分下的经验值换一个随机种子重新划分训练集最优 C 可能就变了。Standardize设置为 false 是因为数据在送入 SVM 之前已经做过归一化了再做一次反而可能破坏 PCA 投影后的数值结构。这里有一个值得注意的点fitcecoc内部会为每个二分类器单独做交叉验证选参数所以训练时间会随着类别数上升9 类地物意味着 36 个二分类器这个训练过程在普通笔记本上大约要跑几分钟到十几分钟。如果你发现训练时间异常长优先检查是不是上一环节的降维没有生效原始 103 维特征直接喂给 RBF 核 SVM 会让核矩阵计算量爆炸。3.2 KNN 在高光谱上的参数敏感性K 值与距离度量怎么配KNN 的原理简单但用在高光谱上有个先天矛盾波段越多欧氏距离在高维空间中的区分度越差。这就是 Hughes 现象的一种体现——样本量固定时分类精度会随着特征维数先升后降。资源里专门放了一个 knn_hughes_test.m我强烈建议跑一下这个脚本它会把不同特征维数下的 KNN 精度画成曲线你会直观看到精度在哪个维数附近开始掉头向下。% knn_test.m 核心逻辑 % 加载 LDA 降维后的数据 load(data_lda_done.mat); k 5; % 使用 fitcknn 训练模型 knn_model fitcknn(train_x, train_y, ... NumNeighbors, k, ... Distance, euclidean, ... Standardize, false); % 预测 pred_label predict(knn_model, test_x); acc sum(pred_label test_y) / length(test_y); fprintf(KNN (k%d) accuracy: %.4f\n, k, acc);K 值的选择在 KNN 里是最玄学的部分。K 太小容易过拟合噪声K 太大又会让决策边界过于平滑。在 PaviaU 上我实测 K5 到 K11 之间精度差异不大但超过 15 之后精度会明显下降因为各类别的样本量极不均衡有些类别只有几百个样本K 太大时多数投票会被大类别主导。距离度量方面欧氏距离是默认选项但如果你尝试马氏距离需要额外计算协方差矩阵的逆在样本量小于波段数的时候这个逆矩阵根本不存在。这就是为什么 KNN 一定搭配 PCA 或 LDA 使用先把维数压到 30 以下再考虑马氏距离才有意义。KNN 还有个别名叫做惰性学习因为它不显式训练模型所有计算都留到预测阶段。这也解释了为什么资源里 KNN 的测试脚本是独立的模型训练几乎零成本真正的耗时在预测时对每个测试样本计算与全部训练样本的距离。测试集约 4000 个样本、训练集约 38000 个样本时KNN 预测一次要在 Matlab 里跑几十秒到几分钟这是正常现象。4. CNN 训练全流程从 cnnsetup.m 到 cnntrain.m 的老牌工具箱路线4.1 DeepLearnToolbox 风格理解这份代码的 CNN 结构这份资源里的 CNN 代码是典型的 DeepLearnToolbox 风格这套工具箱是 Matlab 社区最经典的三层卷积网络实现结构简单、代码可读性极强非常适合学习也适合在课程设计中使用。文件列表里的 cnnsetup.m、cnnff.m、cnnbp.m、cnnapplygrads.m 分别是网络初始化、前向传播、反向传播和参数更新四个环节。% cnnsetup.m 网络结构定义 net.layers { struct(type, i, outputmaps, 1, mapsize, [16 16]) % 输入层 struct(type, c, outputmaps, 6, kernelsize, 5) % 卷积层 1 struct(type, s, scale, 2) % 池化层 1 struct(type, c, outputmaps, 12, kernelsize, 5) % 卷积层 2 struct(type, s, scale, 2) % 池化层 2 };这里的输入层设计成 16×16 的单通道图像意味着原始数据在进入 CNN 之前做了裁剪和重组从每个像素周围取一个 16×16 的邻域块光谱的 103 个波段被压缩成单通道灰度值输入。这种设计在当年的硬件条件下是合理的因为高光谱数据直接以 103 通道输入 CNN 会让参数数量爆炸。但要注意现在的深度学习框架如 PyTorch、TensorFlow 处理高光谱时通常是直接输入 16×16×103 的三维张量用 3D 卷积处理光谱维度。这份资源的 CNN 结构属于简化版精度上限受限于输入表示方式。4.2 训练主循环batch 大小、学习率与梯度检查cnntrain.m 是训练主循环的入口main_cnn_untrain.m 调用了它。训练时要注意几个关键参数batch 大小、学习率、动量项和权值衰减。DeepLearnToolbox 默认的 batch 大小是 50 左右学习率 1动量 0.9。这些参数在小数据集上是经验值但如果把 batch 调太大收敛速度会变慢调太小梯度噪声太大。% cnntrain.m 训练循环关键代码 opts.alpha 1; % 学习率 opts.batchsize 50; % 每个 batch 的样本数 opts.numepochs 10; % 训练轮数 m size(train_x, 3); % 训练样本总数 % 随机打乱样本顺序 rng(0); train_x train_x(:, :, randperm(m)); % 逐 epoch 迭代 for epoch 1 : opts.numepochs for batch 1 : num_batches % 取当前 batch 的数据 batch_x train_x(:, :, indices); batch_y train_y(:, indices); % 前向传播 net cnnff(net, batch_x); % 反向传播 net cnnbp(net, batch_y); % 更新梯度 net cnnapplygrads(net, opts); end end一个非常值得借鉴的细节是rng(0)这行——固定随机种子。深度学习训练天然带有随机性如果你不固定种子每次跑出来的精度都不同论文里就没法写“我们的方法精度是 96.5%”这种可复现的结果。cnnnumgradcheck.m这个脚本是数值梯度检查工具它用中心差分法计算数值梯度和反向传播得到的解析梯度做对比。我第一次跑这个脚本时发现梯度差异在 10 的负 9 次方级别说明网络实现没有 bug可以放心训练。CNN 训练完成后资源里已经附带了一个 trainednet.mat这是作者已经训练好的网络权重。我用它直接跑了一遍测试集得到了精度 baseline再和自己训练的模型对比能快速判断复现过程是否有误。这个思路非常实用先加载预训练模型验证代码链路通不通再从头训练看能不能复现同等水平的精度。5. 避坑指南PaviaU 分类中常见的五个翻车点5.1 内存不足全量数据一次性加载导致 Matlab 直接崩溃现象运行 data_undone.m 时Matlab 报错“Out of memory”或者系统直接无响应。原因PaviaU 原始数据是 610×340×103 的 uint16 类型理论上只占约 42MB 内存但是当你用squeeze逐像素提取时如果中间变量不小心变成了 double 类型内存占用会翻 8 倍。更常见的问题是在 PCA 阶段有人直接用pca(data)函数这个函数会先计算全部样本的协方差矩阵四万样本 × 103 维的协方差矩阵虽然只有 103×103但如果写成data * data就成了四万×四万的矩阵直接内存爆炸。解决不要一次性加载整个 PaviaU.mat 后再做处理应该先提取出有标签的像素并立即转成单精度。代码里用single()包裹数据配合clear释放原始影像变量。另外eigs比eig更适合只需要前 K 个特征向量的场景。5.2 数据泄漏先归一化再划分数据集导致精度虚高现象SVM 在测试集上精度达到 98% 以上结果自己都觉得离谱换一个数据集划分方式后精度暴跌到 85%。原因这是高光谱分类最经典的错误。预处理脚本里如果先对整份数据包括测试集计算均值和标准差再划分训练集和测试集那么测试集的统计信息已经通过均值隐式进入了训练过程。学术上把这种情况叫数据泄漏它让测试精度虚高论文审稿人一眼就能看出来。解决正确的做法是先划分训练集和测试集然后在训练集上计算均值和标准差用这套参数去归一化测试集。顺序反了精度数据就不可信了。我在用这份资源时会把归一化代码单独拆出来确保它只读取训练集计算统计数据。5.3 标签错位find 之后逐像素提取的顺序和标签对不上现象训练的精度大约等于随机猜测损失函数几乎不下降。原因高光谱影像的真值图 PaviaU_gt.mat 虽然和 PaviaU.mat 尺寸一致但用find提取时如果你先找了数据索引又单独用另一套逻辑找了标签索引两套索引的排序方式不同就会错位。特别是当你在中间步骤用sort或者unique处理过索引时顺序极容易被破坏。解决索引必须同源。我习惯把索引 idx 定义一次之后对数据、对标签都用同一套 idx 来取像第 2 章里的示例代码一样。每次修改完预处理脚本后用assert(length(idx) length(labels))这类断言检查样本数和标签数是否一致再检查前 20 个标签的分布是否合理。5.4 fitcecoc 在旧版 Matlab 上不支持现象运行 SVM 代码时报错“Undefined function or variable fitcecoc”。原因fitcecoc 是在 Matlab 2014b 之后才加入统计工具箱的函数如果你的版本更老这个函数根本不存在。很多课程用的还是校园机房里的老版本 Matlab这种问题非常常见。解决看资源里的程序说明.txt 有没有标明版本要求如果没有直接用svmtrain替代。注意svmtrain默认只支持二分类多分类需要循环训练多个模型再投票代码会复杂不少。另一个方案是直接用 libsvm 的 matlab 接口它支持多分类且跨版本兼容性更好只是需要先编译 mex 文件。5.5 CNN 输入尺寸不匹配16×16 邻域裁剪在影像边界失效现象CNN 训练时报错“Subscript indices must either be real positive integers or logicals”或者精度极低。原因从影像中裁剪 16×16 邻域块时如果一个像素位于影像边缘 8 像素以内它的邻域超出了图像边界取不到完整的块。如果你直接忽略这些像素会导致边缘地物的样本全部丢失而 PaviaU 的某些类别恰恰集中在影像边缘。解决常见做法是做镜像填充或者零填充让影像边缘的像素也能取到完整的邻域。更省事的做法是只保留距离边界至少 8 像素内的样本在代码里用r 8 r rows-8 c 8 c cols-8这样的条件过滤一遍。这两种方案的取舍在于填充会引入虚假信息丢弃会损失边缘样本具体选哪个取决于你更在意精度还是更在意类别的空间完整性。6. 验证 CNN 是否真的收敛精度曲线与梯度检查联动前面提到 cnntrain.m 里设置了 10 个 epoch但你怎么知道 10 个 epoch 够不够盲目增加 epoch 可能导致过拟合盲目减少又可能欠拟合。我推荐的做法是把训练过程的损失曲线打印出来资源里的代码只输出最终精度但你在训练循环里加两行就能画出损失曲线。% cnntrain.m 中嵌入损失记录 loss_history zeros(1, opts.numepochs * num_batches); iter 0; for epoch 1 : opts.numepochs for batch 1 : num_batches iter iter 1; % 前向传播和反向传播 net cnnff(net, batch_x); net cnnbp(net, batch_y); % 计算损失交叉熵或均方误差 loss_history(iter) net.loss; net cnnapplygrads(net, opts); end end % 画损失曲线 figure; plot(loss_history); xlabel(Iteration); ylabel(Loss); title(CNN Training Loss Curve);每一轮迭代后的 loss 值在 cnnbp.m 里已经计算好了你只需要把它存下来。如果损失曲线在最后几个 epoch 还在明显下降说明训练不够可以加大 numepochs 或者调低 alpha如果曲线早就平坦甚至开始上升说明过拟合了应该早停。配合 cnnnumgradcheck.m 的梯度检查结果你可以确认反向传播实现的正确性然后放心地把锅甩给超参数调优。还有一个验证模型是否学到有效特征的办法把第一个卷积层的卷积核可视化。CNN 第一层学到的是最基础的边缘和纹理特征如果卷积核看起来像是随机噪声说明网络没有收敛到有效解如果能看到明显的边缘响应模式说明特征提取已经生效。DeepLearnToolbox 里可以用figure; montage(net.layers{1}.k{1})查看第一层卷积核这一步虽然不影响分类精度但对判断训练过程是否健康很有帮助。对于 trainednet.mat 这个文件我通常先加载它跑一次测试集记录精度然后重新初始化网络从头训练。如果从头训练的最终精度和预训练模型相差超过 3 个百分点大概率是数据划分不一致或者归一化参数对不上需要回头检查预处理流程。从那以后我每次做高光谱分类对比实验都强制走一遍这套流程先固定随机种子再统一数据划分最后才让三个模型逐一出场。多跑几轮之后你会发现SVM 和 KNN 的分数差距远没有论文里说的那么悬殊关键全在预处理和超参数上。希望这份拆解能帮你在 PaviaU 上少走几步弯路。本文还有配套的精品资源点击获取