简介:文档系统梳理了应用特征感知与协同表示(FP-CRC)的高光谱图像分类方法,面向遥感图像处理、机器学习及高光谱分析领域的研究者与工程人员。内容从协同表示基本框架出发,介绍多层稀疏表示、联合类内协同表示、空间感知协同表示、区域自适应与字典自适应协同表示、加权正则化协同表示、多特征字典学习协同表示、概率多核协同表示等代表性算法的思路与局限,并重点阐述FP-CRC在图像重建、光谱特征感知和空间特征感知三方面的改进,配合公式推导与流程图,帮助读者理解从问题建模到分类实现的完整过程。资源包共1个文件,为docx格式,大小436KB,适合直接阅读、标记批注或作为论文写作参考。目前已有108人学习下载,对需要快速掌握高光谱图像分类前沿方法或开展相关研究的读者,是一份精炼且有参考价值的文献资料。 高光谱图像的分类问题,本质上是“高维但小样本”的博弈:一幅图动辄上百个波段,相邻波段高度相关,真正起判别作用的可能就十几个;而训练标注像素又少得可怜。特征感知与协同表示的组合,正是冲着这两点来的——先用特征感知把波段和局部邻域里有用的信息挑出来,再用协同表示让全体训练样本共同参与决策,而不是像稀疏表示那样强行拉少数原子。这套路在常见的公开高光谱数据集上通常能把总体分类精度提升3到8个百分点,而且不需要很大计算代价。文章适合正在做遥感地物分类、或者想把协同表示玩明白的同学,照着步骤能直接复现。
2. 协同表示分类的核心原理:为什么高光谱分类需要它
2.1 高光谱图像和传统分类流程不匹配在哪
普通RGB图像只有三个波段,高光谱图像动辄上百个波段,数据在内存里的组织形式是一个三维立方体:宽、高、光谱维。对某个像素做分类,本质上是对它的一维光谱向量做判别。但这个向量太长了,相邻波段之间相关性极高,直接丢给K近邻或支持向量机时,会撞上高维空间里的维数灾难:维度越高、样本越稀,距离度量越不可靠。
我最早入行时,习惯先把所有波段铺平成一个大矩阵,然后直接做PCA降维,再丢给随机森林。这条链路能跑,但有两个问题:PCA是无监督的,它保留的是方差最大的方向,不等于判别力最强的方向;随机森林对高光谱里强相关的波段特征做分裂,容易把冗余权重分散。它们都没解决好“特征感知”这件事。
高光谱分类的突破口不在于换更强的分类器,而在于先解决“用什么特征去分类”。特征感知要做的就是让特征从原始波段里被有针对性地筛选和加权;协同表示分类则是基于这个特征空间去坐最后的判决。两者前后串起来,刚好补上传统流程的空白。
2.2 从稀疏表示到协同表示:L2正则化的解析解
稀疏表示分类(SRC)的思路是用全体训练样本作字典,去线性表示测试样本,但约束是系数尽可能稀疏,也就是让少数样本说话。这个约束放在人脸识别里很漂亮,但在高光谱上水土不服:高光谱不同地物的光谱曲线彼此交叠很严重,强制稀往往选中了错误的字典原子,而且L1范数没有解析解,每次都要迭代求解,计算量偏大。
协同表示分类(CRC)把L1正则换成L2正则,让所有类别的训练样本共同参与表示,数学上写成:
min_{α} ||y - Dα||_2² + λ||α||_2²其中y是测试样本的特征向量,D是全体训练样本构成的字典矩阵,每一列一个原子,α是对应的表示系数,λ是正则化参数。由于是L2正则,解析解直接可得:
α = (D^T D + λI)^{-1} D^T y注意这个式子里的投影矩阵P=(D^T D + λI)^{-1} D^T只依赖于字典D,和测试样本y无关。训练阶段把这个矩阵算好缓存住,测试阶段对每个样本只需要一次矩阵乘法就能得到系数,速度极快。我在17000多个测试像素上做过对比,同样特征的条件下,CRC比SRC快至少一个数量级,而且精度不降。
2.3 分类判决:各类重建残差怎么用
得到全局系数α后,并不会直接拿它预测类别,而是要分两步走。先把α按类别拆开:α_c表示只属于第c类的那部分系数,其余类的系数一律当成零。然后用每个类别自己的字典原子D_c和系数α_c去重建测试样本y,得到残差:
r_c = ||y - D_c α_c||₂最后取残差最小的那个类别作为预测结果。这个设计很巧妙:虽然系数是全体样本共同求出来的,但判决时只看每一类单独重建得怎么样。某类地物如果是测试样本真正的归属,它在协同表示里的贡献会集中到自己类别的原子,重建误差自然会小;其他类的原子则被挤到不起眼的位置,重建出来的东西偏差很大。
2.4 特征感知和协同表示的分工逻辑与协同嵌入
既然CRC的输入特征直接决定残差质量,特征感知的核心任务就是把原始高光谱数据映射到更适合协同表示的特征空间。一个最常见的做法是在特征感知时用判别性的波段加权,而CRC训练和分类也同时在这一特征空间里进行。这样做的好处很直接:平滑类内样本,拉升类间距离,让残差判决更干净。
实际管线是端到端串起来的:先把高光谱数据做归一化和必要去噪,然后用特征感知模块对原始光谱或者邻域块做加权变换,再从特征感知的结果中抽取训练集和测试集。字典在特征空间里构建,CRC的投影矩阵也在同一空间里训练。如果特征感知改变了特征空间,字典必须同步重建,这个顺序不能搞反,后面避坑章节我会细讲。
3. 特征感知模块构建:从波段到空间邻域两步走
3.1 特征感知从哪里开始:归一化与波段筛查
特征感知的第一步不是设计花哨的卷积结构,而是先把最基础的数据清洗做掉。高光谱数据里有几种波段是典型的干扰项:大气吸收波段信噪比极低,出现的全是噪声条纹;有的波段在同类地物内部差异甚至大于类间差异,对判别毫无帮助。我的做法是先查看整体波段的均值光谱曲线,把那些曲线毛刺特别剧烈、样本能量明显异常的波段直接删掉,这比任何算法加权都来得可靠。
接着是归一化。不少教程默认按像素做归一化,也就是每个像素的光谱向量除以自己最大值,这在多数场景下会破坏绝对反射率信息,因为同类地物不同像素之间的明暗差异会被抹平。更稳妥的是按波段做标准化:每个波段在所有训练像素上求均值和方差,然后整体转换,让每个波段落在统一的能量尺度上。特征感知的加权值也正是基于这个归一化后的特征来计算。
3.2 光谱感知:用Fisher判别比分波段加权
特征感知里最轻量、最不容易翻车的实现,是给每个波段算一个判别力权重,然后把原始光谱乘上这个权重。判别力可以用Fisher比分来量化:分子是各个类别波段均值的离散程度,分母是各类内部方差的加和,比值越大说明这个波段越能区分不同地物。
下面这段代码实现Fisher权重:
import numpy as np def fisher_weight(X, y, eps=1e-6): """ 计算每个光谱波段的 Fisher 判别权重。 X: 训练特征矩阵,形状 (n_samples, n_bands) y: 训练标签,形状 (n_samples,) """ classes = np.unique(y) global_mean = X.mean(axis=0) between = np.zeros(X.shape[1]) within = np.zeros(X.shape[1]) for c in classes: X_c = X[y == c] mean_c = X_c.mean(axis=0) n_c = X_c.shape[0] # 类间离散程度:类别均值与全局均值的差距 between += n_c * (mean_c - global_mean) ** 2 # 类内离散程度:类内样本的方差累加 within += ((X_c - mean_c) ** 2).sum(axis=0) fisher_score = between / (within + eps) fisher_score = fisher_score / (fisher_score.max() + eps) return fisher_score这里的关键参数是eps,作用是在类内方差接近零的波段上防止除零溢出。需要注意,如果某个波段在所有类别里都是一条水平线,它的Fisher分会接近零,加权后会被压制到几乎不参与后续计算;而判别力强的波段权重会接近1,成为协同表示的主要依据。
实际使用时,我会把X换成归一化后的矩阵,并把得到的权重直接乘到训练和测试特征上。有一个坑:Fisher权重必须在训练集上计算,不能混入测试集,否则等于变相偷看了测试数据的信息,精度虚高。
3.3 空间特征感知:局部邻域的整形与拼接
光谱特征感知只用到了像元本身的一条曲线,而高光谱图像还有一个天然优势——空间连续性。同一块地物在空间上往往是成片出现的,相邻像素的光谱高度相似。把这种邻域信息纳入特征感知,能显著压低单像素的噪声干扰。
常见的做法是以目标像素为中心开一个大小为p×p的窗口,把窗口内所有像素的光谱向量直接拼接成一维特征,或者先求邻域均值再拼上中心像素原始光谱。窗口大小一般取3到9,太大会跨到别的类别边缘,反而把特征搞浑。我通常用5×5窗口,先取窗口内光谱均值,再和中心像素拼接,得到一个维度为2倍波段数的特征向量。
这里有一个空间感知的变体值得试试:对窗口内每个像素和中心像素求光谱角距离,把距离值作为空间权重视角下的邻域聚合依据。光谱角距离定义为两个光谱向量之间的夹角余弦,它比欧氏距离对光照强度变化更鲁棒,在高光谱里很实用。不过它把每个邻域像素的加权变成了非线性操作,协同表示里的线性字典就不好直接表达了,所以实践中我还是优先用均值拼接,损失一点空间感知的精确度,但保住协同表示的速度。
3.4 特征感知模块的落地封装与输出规范
为了让整条链路可复现,特征感知应该封装成一个独立的变换类,输入原始数据和标签,输出变换后的特征矩阵。这样训练集和测试集可以用同一套参数变换,不会出现不一致。以下是一个简洁的封装示例:
class FeatureAwareTransform: def __init__(self, mode='fisher', patch_size=0): self.mode = mode self.patch_size = patch_size self.band_weight = None def fit(self, X_patches, y_train): # X_patches 是经过 patch 处理后的特征,也可能是原始像素 if self.mode == 'fisher': self.band_weight = fisher_weight(X_patches, y_train) return self def transform(self, X_patches): if self.band_weight is not None: return X_patches * self.band_weight return X_patches这个封装的要点是fit和transform分开,fit只吃训练数据,transform对训练和测试一视同仁。后续做交叉验证时会特别安心,因为不会把测试集的信息泄漏到特征加权里。波段权重在实际数据上最好看一眼,如果出现某个波段权重异常高、其他全部接近零,说明类内方差计算里可能有样本过少或者噪声异常,要回到数据清洗步骤排查。
4. 协同表示分类器实现与参数设置:先跑通再优化
4.1 用Python实现协同表示分类器的最小完整代码
理解了CRC的数学原理,实现就非常直接。核心工作是两件事:训练阶段预计算投影矩阵,测试阶段对每个样本求系数并计算各类重建残差。下面给一个完整的numpy实现,没有外部依赖,复制就能用:
import numpy as np class CollaborativeRepresentationClassifier: def __init__(self, reg_lambda=1e-2): self.reg_lambda = reg_lambda self.D = None # 字典矩阵,每行一个原子 self.y = None # 字典原子对应的标签 self.classes = None self.P = None # 预计算投影矩阵 def fit(self, X_train, y_train): self.D = np.asarray(X_train, dtype=np.float64) self.y = np.asarray(y_train) self.classes = np.unique(self.y) d = self.D.shape[1] # 求解投影矩阵 P = (D^T D + λI)^{-1} D^T DtD = self.D.T @ self.D self.P = np.linalg.solve( DtD + self.reg_lambda * np.eye(d), self.D.T ) def predict(self, X_test): preds = [] for x in np.asarray(X_test): alpha = self.P @ x best_class = None best_residual = float('inf') for c in self.classes: idx = np.where(self.y == c)[0] # 只用当前类别对应的系数做重建 x_recon = self.D[idx] @ alpha[idx] residual = np.linalg.norm(x - x_recon) ** 2 if residual < best_residual: best_residual = residual best_class = c preds.append(best_class) return np.array(preds)代码里有两个值得注意的细节。一是用np.linalg.solve而不是np.linalg.inv,因为solve直接做LU分解,数值稳定性更好,速度也更快。二是预测时对每个测试样本都要遍历一次类别,外观上像两层循环,但核心矩阵乘法已经由预计算P承担,内层只是索引和残差计算,所以实际跑起来依然很快。
配合特征感知模块的完整调用方式如下:先对训练特征做FA_transform.fit和transform,再在scikit-learn里划分训练集测试集,最后实例化CRC模型训练预测。测试集的样本不需要再单独做任何拟合操作。
4.2 字典构造与每类原子数量的选择
CRC的字典是直接拿训练样本拼成的,不需要像传统字典学习那样iteration训练。但每类放多少原子进去很有讲究。如果某类训练样本太多,字典里这一类占了绝对优势,协同表示求出来的系数自然偏爱它,分类精度会被样本最多的类别绑架。如果某类样本太少,则该类的重建误差波动很大,容易误判。
我的经验是每类取50到200个原子,具体根据总样本量调配。超过200个之后,矩阵D^T D的规模随之变大,求逆成本快速上升,但分类精度的提升却趋于平缓。做高光谱的公开数据集时,每类100个原子已经是性价比很高的配置。
还需注意,如果特征感知引入了patch窗口,不同像素之间存在空间重叠,直接随机抽样会让训练集和测试集共享部分邻域信息,精度虚高。更严格的划分方式是按空间区域切割,或者先对图像做像素筛选,再按块划分训练测试集。这块在避坑章节会展开。
4.3 协同表示的必调参数:λ、特征维度与窗口大小
协同表示分类虽然参数不多,但每一个都很吃数据。第一个是正则化参数λ。它控制系数解的平滑程度:λ太小,D^T D接近奇异,求逆不稳定;λ太大,系数被压缩得过狠,各类重建残差都变大,判别能力被削弱。在特征归一化到0到1之间的前提下,λ从0.01开始调,用对数网格在1e-4到1之间搜索是常见的做法。我在Indian Pines数据集上,λ取0.01左右就能得到不错结果;如果特征做了标准化且包含负值,λ要适当放大到0.1量级。
第二个是特征维度。经过Fisher加权后,特征维度和原始波段数一致,通常会有大量弱判别波段权重接近零,它们还在占用字典矩阵的列空间。我的做法是在特征感知后叠加一个PCA截断,把维度压到30到60。这样协同表示里D^T D的规模从几百下降到几十,求逆稳定性显著改善,精度反而经常提升。
第三个是窗口大小。它和地物尺度密切相关,田块纹理粗的地方用7×7不会跨边界,城市建筑密集区用3×3更稳妥。这个参数没法一口吃成胖子,我会做一组网格搜索,结合分类评估指标来定。后面验证章节会教你怎么系统评估。
4.4 五分类小实验:从特征感知到协同表示的完整流水线
为了把前面几节串起来,这里给一个完整但精简的流水线示例,输入是已经切割好的patch矩阵:
from sklearn.decomposition import PCA from sklearn.model_selection import train_test_split # X_raw: 原始特征,y: 标签,假设已经按 patch 做好了 fa = FeatureAwareTransform(mode='fisher') fa.fit(X_raw_train, y_train) X_fa_train = fa.transform(X_raw_train) X_fa_test = fa.transform(X_raw_test) # PCA 压缩到 50 维 pca = PCA(n_components=50) X_pca_train = pca.fit_transform(X_fa_train) X_pca_test = pca.transform(X_fa_test) # 协同表示分类 crc = CollaborativeRepresentationClassifier(reg_lambda=0.01) crc.fit(X_pca_train, y_train) y_pred = crc.predict(X_pca_test)这条流水线每次都能稳定跑通,作为基准参照非常合适。后续改动特征感知模块或者调整λ,都在这条链路上做对照。
5. 避坑清单:高光谱分类实战中的常见问题与排查
5.1 训练测试划分不干净导致精度虚高
一旦开了patch窗口,像素之间就会产生空间重叠。如果不加控制地随机划分训练集测试集,同一个5×5窗口里的像素可能一部分进了训练集、另一部分进了测试集,模型等于提前见过邻居信息,测试精度虚高得离谱。我见过有人报出99.8%的精度,比正常结果高出近10个百分点,很不正常。
解决的办法是按图块划分。把图像分割成若干互不重叠的方块区域,一部分区域拿来做训练、一部分做测试,边界上扩一圈窗口时也不允许从测试区借用信息。这个做法会让精度数字下降一些,但才是真实反映模型泛化能力的结果。
5.2 λ取值像玄学:先看特征尺度再调参
同一个λ值,在A数据集上效果好,换到B数据集上一塌糊涂,这种体验在CRC上非常常见。根源在于λ是和特征尺度绑定的。如果特征数值整体在0到1之间,D^T D的对角元素大体在10量级,λ取0.01是合理的;如果特征没有归一化、数值在几千的量级,D^T D对角元素动辄上百万,0.01的λ等于不存在,D^T D照样病态。
排查时先打印D^T D的迹或最大特征值,然后让λ比这个特征值小一到两个数量级。我一般以1e-2为起点,跑一个对数网格搜索,再根据分类评估指标选择最优值,不让λ靠感觉拍脑袋。
5.3 波段冗余和残留噪声波段拖累特征感知
特征感知的加权基于Fisher判别分,但它只考虑了单个波段的判别能力,没考虑波段之间的相关性。高光谱里经常有十几二十个波段高度相关,Fisher分会给它们都赋予较高权重,但它们提供的其实是同一条信息,等于把冗余特征放大了好几倍。
排查方法是看特征感知后的相关矩阵,如果存在一团相关性超过0.95的波段组,就要做一次PCA或者删除其中一个代表波段。我习惯先做波段分组去相关,再做Fisher加权,顺序不要反过来,否则加权会把冗余放大得更加厉害。
5.4 小样本条件下特征感知过拟合
当训练样本特别少时,Fisher权重很容易被个别异常样本带偏。比如某类地物只有十几个像素,其中一个像素因为云阴影导致光谱异常,它会把该类均值拉偏,波段权重计算出现明显波动。这种过拟合在测试集上表现为整体精度下降,但局部某个类的精度暴跌。
解决的办法是给Fisher权重的分子增加一撮平滑,或者在类别样本量过少时直接退化为不加权特征。另一个经验是对权重做时序平滑,高光谱相邻波段的物理含义是连续的,权重突变往往代表噪声干扰,用滑动平均把权重曲线拉平,效果常常出乎意料地好。
5.5 计算开销爆炸:矩阵规模失控
协同表示里最重的操作是求D^T D的逆,当特征维度和字典原子数量都上去以后,这个操作会吃掉大量内存。我曾在特征维度200、原子数5000的情况下,直接让内存溢出,进程被杀。这里的一个常见误用是没有压缩特征就硬跑。
解决的方式有两板斧:第一,特征感知后接PCA降维,把特征维度压到50甚至更低;第二,限制每类原子数量。如果还是不够,可以换用迭代求解,比如用共轭梯度法近似求α,不必精确求逆,速度会快很多,代价是精度略有下降。高光谱数据量本身不算小,算力评估一定要放在前头,别等内存溢出了才回头改。
6. 验证技巧与进阶调整:让分类评估指标真正反映模型水平
6.1 用OA、AA和Kappa做三指标对照,别只看Overall Accuracy
高光谱分类的标准评估配置不是打印一个混淆矩阵就完事。总体精度OA是所有被正确分类像素占总数的比例,但它会被样本多的类别带节奏。比如一个数据集中农田类占了一半像素,农田分类精度高就拉动OA虚高,而植被类和建筑类分得稀烂也看不出来。所以还要看平均精度AA,它是每个类别精度的算术平均,对样本少的类别更敏感。第三个是Kappa系数,它把随机猜对的概率剔除掉,数值在0到1之间,分数越接近1说明一致性越强。
交叉验证也要配合同样的三指标。我习惯用5折,在每一折里重新做特征感知、PCA、CRC训练和评估,最后把五折的OA、AA、Kappa取平均和标准差。标准差尤其重要,如果同一组参数下五折结果差到5个百分点以上,说明划分方式对结果影响过大,需要回到按块划分的方案重新设计。
6.2 进阶技巧:把特征感知和协同表示端到端联动优化
到这里,基础方案已经能稳定跑了。如果你想在这个方向继续深入,有一个性价比很高的进阶路径:把特征感知的波段权重从Fisher固定值改成可学习参数,让协同表示的残差作为损失去反向传播更新权重。具体做法是先让特征感知模块输出和原始波段等长的可训练权向量,分类器部分依然计算CRC的各类重建残差,损失就用一个对残差做归一化指数映射的交叉熵来定义。整套东西可以包装成轻量的autograd模块,在GPU上用小学习率跑几十轮就能收敛。
这也是特征感知这个方向的魅力所在:它和协同表示的组合天然具备可微性,不像稀疏表示那样到处是断点。当你把固定Fisher权重换成可学习参数后,会有一种模型自己在高光谱波段里找重点的感觉。
最后说一句个人的体会:遇到精度瓶颈时,先别急着换更复杂的网络结构,老老实实检查数据划分是否泄漏、特征感知的加权是不是被冗余波段污染、λ有没有跟特征尺度对齐,这三处往往就是分类评估指标上不去的根源。把每一处细节都过一遍之后,你会发现在高光谱这种数据上,简洁的方案搭配正确的细节,远比堆砌复杂模块可靠得多。希望帮到你。
本文还有配套的精品资源,点击获取