ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

小样本高光谱图像分类:Python源码实现与3D+2D混合CNN详解

小样本高光谱图像分类:Python源码实现与3D+2D混合CNN详解 简介面向小样本高光谱图像分类的深度学习源码包适合研究遥感图像处理、机器学习及计算机视觉的学生与工程师。项目围绕训练样本不足的难点基于卷积神经网络等深度模型结合迁移学习与数据增强策略提供从数据预处理、网络模型构建、训练验证到结果评估的完整实现。压缩包内含三十六个文件以Python脚本、numpy数据文件、可视化结果图片等为主体另附训练好的模型权重、工程配置文件及Markdown说明文档整体大小约六十二兆字节。目前已有三百九十五人学习下载。说明文档逐一介绍代码用法、模型选型依据、数据集构建流程与实验结果并支持在Indian Pines等公开数据上复现能帮助读者快速掌握小样本高光谱分类的工程实践方法。1. 小样本高光谱图像分类为什么这套 Python 源码方案值得你跑一遍做高光谱图像分类的人十有八九会被同一个问题卡住标记样本太少。一个场景几百个波段一块地里的像元成千上万可你手里带标签的像素每类往往只有 5 到 30 个。传统 SVM、随机森林在这种“小样本、高维度”的组合下很容易过拟合深度学习的直觉又告诉你“数据少就别碰 CNN”。但这套基于深度学习实现小样本高光谱图像分类的 Python 方案恰恰反着来利用空间邻域做 patch 采样用 3D2D 混合卷积把光谱-空间特征抽出来让模型在极少样本下照样训稳。这套源码适合搞遥感地物识别、精细农业、矿物填图或者想把“小样本高光谱深度学习”这条技术路线快速跑通、拿去复用到自己数据集上的人。2. 高光谱图像分类的核心原理三维数据、小样本难点与混合卷积的选型理由2.1 高光谱图像的“数据形态”从 mat 文件到 (height, width, bands) 的三维张量高光谱图像和普通 RGB 图像最大的区别在光谱维。RGB 只有 3 个波段高光谱成像仪通常在 400~2500nm 范围内采集 100 到 300 个连续波段。存到 mat 文件里数据形态一般是两种(h, w, bands)或(bands, h, w)。不少开源数据集为了让读取方便把波段维放在第一个维度例如 Pavia University 的原始数据就是(103, 610, 340)的形状。我在复现这套源码时第一步永远是检查形状而不是直接套网络。因为后面切 patch、做归一化、送进 PyTorch 的张量全都依赖“波段维在最后一个维度”这个约定。如果波段维在最前面直接送进卷积会得到完全错误的结果而且这种错误不会报错只会慢慢把精度磨没。源码里常见做法是加一个维度重排的逻辑我一般会写成这样import scipy.io as sio import numpy as np def load_hsi_mat(mat_path, data_keyX, label_keyy): mat sio.loadmat(mat_path) X mat[data_key] # 可能是 (bands, h, w) 也可能是 (h, w, bands) y mat[label_key] # 标签0 通常表示背景 if X.ndim ! 3: raise ValueError(需要三维高光谱立方体) if X.shape[0] X.shape[2]: # (bands, h, w) - (h, w, bands) X np.transpose(X, (1, 2, 0)) return X.astype(np.float32), y.astype(np.int32)这段代码的逻辑很简单先看波段维是不是最小的那个维度如果是就转置。因为绝大多数高光谱数据集的波段数几十到几百都大于空间维度数除非遇到特别小的裁剪块但那种情况极少。做完这一步X 的 shape 就统一成(h, w, bands)后面所有预处理都能对齐。2.2 小样本为什么是深度学习的一道坎维度灾难与过拟合小样本和深度学习乍一听是矛盾的。高光谱数据每个像素有几百个波段相当于几百维特征而训练样本只有几十个。特征维度远大于样本数传统机器学习很容易发生维度灾难SVM 靠核函数还能撑住但深度学习模型动辄几万到几十万个参数几十个样本根本约束不住模型容量。但高光谱图像有一个普通表格数据没有的特性空间相关性。相邻像素大概率属于同一类地物所以把一个像素连同它周围 5×5 或 11×11 的邻域一起看等于把“一个像素样本”扩展成了“一个空间 patch 样本”。一个 patch 里有几十个像素模型学到的不再是单像素的光谱曲线而是光谱空间纹理的组合模式。这也是为什么小样本高光谱分类里patch 切分几乎是所有深度学习方案的标配。在小样本场景下模型结构也要刻意变小。这套源码里的做法是控制卷积核数量第一层 3D 卷积只输出 8 个特征图而不是像 ImageNet 那类任务动辄 64 个起步。特征图少参数就少几十个样本才有机会把模型驯服。后面 FC 层的隐藏单元也被压缩到 256 以内整网参数量控制在几十万这个量级既足够拟合高光谱特征又不至于直接过拟合到训练集上。2.3 光谱-空间特征与混合 CNN为什么 3D 卷积后面还要接 2D 卷积高光谱分类早期主流做法是逐像素分类把每个像素的整条光谱曲线当成一维信号用 1D-CNN 或 SVM 处理。但这种做法忽略了一个信息目标地物的空间纹理。农田边界、屋顶形状、矿物蚀变带这些特征单靠一条光谱曲线是看不出来的。3D-CNN 能同时卷积光谱维和空间维适合在浅层提取“光谱空间”的联合特征。但 3D 卷积的参数量和计算量都比 2D 卷积高一截直接全用 3D 卷积到深层小样本下一定会爆。于是出现了 3D2D 混合结构前几层用 3D 卷积把光谱维的信息压缩、融合后几层转成 2D 卷积专注空间纹理同时把参数量压下来。这套源码的核心模型就是这个混合思路。import torch.nn as nn class HybridHSINet(nn.Module): def __init__(self, n_bands, n_classes): super().__init__() # 光谱维压缩层 self.conv3d_1 nn.Conv3d(1, 8, kernel_size(7, 3, 3), stride(2, 1, 1)) self.conv3d_2 nn.Conv3d(8, 16, kernel_size(5, 3, 3), stride(1, 1, 1)) self.conv3d_3 nn.Conv3d(16, 32, kernel_size(3, 3, 3), stride(1, 1, 1)) # 空间残差层 self.conv2d_1 nn.Conv2d(32, 64, kernel_size3, padding1) self.dropout nn.Dropout(0.5) def forward(self, x): # x: (batch, band, h, w) 先补一个通道维给 Conv3d x x.unsqueeze(1) x self.conv3d_1(x) x self.conv3d_2(x) x self.conv3d_3(x) # 转成 4D 张量 b, c, d, h, w x.shape x x.squeeze(2) # 光谱维被压到1时直接去掉 x self.conv2d_1(x) x self.dropout(x) return x这段代码是简化版但结构逻辑和源码完全一致3D 卷积逐步压缩光谱维等到光谱维变成 1 时把数据降到 2D 空间卷积。这里的关键点是kernel_size的第一个维度7、5、3逐层缩小目的是先把光谱上的长距离依赖压缩完再进入纯空间特征提取避免 3D 卷积一直维持高光谱维度导致参数爆炸。2.4 常见数据集与预处理流程Indian Pines、Pavia University 与 Salinas 的差异做高光谱分类绕不开这三个公开数据。Indian Pines 是印第安纳州一块农业区域的 AVIRIS 影像空间尺寸 145×145去掉噪声波段后常用 200 个波段16 类地物Pavia University 是意大利帕维亚大学周边的 ROSIS 影像107 万像素103 波段但空间分辨率更高Salinas 是加州 Salinas Valley 的影像512×217204 波段16 类。三者波段数量、地物类型、空间尺度都不同跨数据集验证几乎是必须做的动作。数据集空间尺寸波段数类别数样本规模典型难点Indian Pines145×14520016约 1 万类别极不平衡多数类占比超 30%Pavia University610×3401039约 4 万空间尺度大训练时 patch 采样分布要均匀Salinas512×21720416约 5 万行间条纹噪声多需去噪处理预处理流程里最容易被忽略的是“去吸水带”和“归一化”。Indian Pines 原始 220 波段里含 20 个受水和大气吸收影响的波段很多方案直接删掉留 200 波段Pavia 只有 103 波段吸水带去不去影响没那么大但数据范围需要做 min-max 归一化。我在这套源码里见过的做法是先按波段索引列黑名单去除再对每个波段做独立归一化把每个波段的值压到 [0, 1]而不是对整个立方体做一次归一化。因为不同波段的辐射亮度量级差异很大全局归一化会把弱波段的信息直接抹掉。def normalize_hsi(X, band_ignoreNone): H, W, B X.shape if band_ignore is not None: mask np.ones(B, dtypebool) mask[band_ignore] False X X[:, :, mask] X_norm np.zeros_like(X, dtypenp.float32) for b in range(X.shape[2]): band X[:, :, b] vmin, vmax band.min(), band.max() if vmax - vmin 1e-6: X_norm[:, :, b] (band - vmin) / (vmax - vmin) return X_norm逐波段归一化在源码里承担的功能是把量纲差异抹平。比如第 60 波段的像素值普遍在 8000~12000而第 120 波段可能只有 200~500如果不分开归一化后者对 loss 的贡献会被数值完全淹没。这一步做完后面才能放心把 patch 切成训练集和测试集。3. 用 Python 把源码跑通环境准备、patch 切分、模型训练与指标计算3.1 环境配置与项目目录vscode 里跑通最小依赖源码最常见的语言版本是 Python 3.8~3.10深度学习框架用 PyTorch偶尔看到 TensorFlow 2.x 的版本但 PyTorch 在学术复现里更主流。我在 vscode 里配环境时会先建一个 conda 虚拟环境避免和系统 Python 混在一起。如果还没装过相关依赖顺着 Python 安装教程把环境起来再一条命令装全依赖conda create -n hsi python3.9 conda activate hsi pip install torch torchvision matplotlib scikit-learn scipy numpy这里我把 torch 和 torchvision 分开装是因为很多机器有 CUDA 版本兼容问题。如果显卡驱动是旧的pip install torch会拉到最新版然后torch.cuda.is_available()返回 False代码直接掉到 CPU 上跑一个 epoch 慢出天际。我的习惯是先跑一下python -c import torch; print(torch.cuda.is_available())输出 False 的话就去装对应 CUDA 版本的 torch而不是硬等 CPU 训完 200 个 epoch。3.2 patch 切分把整张高光谱图变成样本总数可控的小块做完归一化下一步是切 patch。patch 的含义是以每个像素为中心取一个 11×11 的邻域这个邻域内的所有波段值合在一起作为这个像素的特征输入。边界像素通过 padding 补零处理整张图切完后样本数量等于原图像素数量减去背景像素数量。def create_patches(X, y, patch_size11, bg_label0): H, W, B X.shape pad patch_size // 2 X_padded np.pad(X, ((pad, pad), (pad, pad), (0, 0)), modereflect) y_padded np.pad(y, pad, modeconstant, constant_valuesbg_label) samples, labels [], [] for i in range(H): for j in range(W): if y[i, j] bg_label: continue patch X_padded[i:ipatch_size, j:jpatch_size, :] samples.append(patch) labels.append(y[i, j]) return np.stack(samples), np.array(labels)这里有个容易踩的细节modereflect是为了让边缘像素的 patch 更自然而不是用 0 硬补。虽然背景不参与训练但边缘地物紧挨着画幅边界时reflect 模式比 constant 模式稳得多。patch_size 默认给 11是因为这个奇数尺寸在 3D 卷积核为 3×3 时做两次卷积后空间特征图尺寸刚好是 1方便后面直接展平成 FC 层输入。切完 patch 后你会得到大量“样本”但其中大量 patch 是高度重叠的。这正是小样本深度学习能跑起来的另一层原因相邻 patch 之间的相似性提供了隐式正则模型见过类似的邻域很多次参数不容易发飘。3.3 模型定义用 PyTorch 实现一个可训练的小型网络模型结构我在第 2 章已经给过简化版这里补一个真正能训练的分类头import torch.nn as nn import torch.nn.functional as F class SmallHybridCNN(nn.Module): def __init__(self, in_bands, n_classes): super().__init__() self.conv3d_1 nn.Conv3d(1, 8, (7, 3, 3), stride(2, 1, 1)) self.bn1 nn.BatchNorm3d(8) self.conv3d_2 nn.Conv3d(8, 16, (5, 3, 3), stride(1, 1, 1)) self.bn2 nn.BatchNorm3d(16) self.flatten nn.Flatten() self.fc nn.Linear(16 * 3 * 1 * 1, 64) self.out nn.Linear(64, n_classes) def forward(self, x): x x.unsqueeze(1) x F.relu(self.bn1(self.conv3d_1(x))) x F.relu(self.bn2(self.conv3d_2(x))) x self.flatten(x) x F.relu(self.fc(x)) x self.out(x) return x这里设定了一个关键约束输入 patch 的空间尺寸是 11×11第一层 3D 卷积空间核是 3×3stride1输出空间仍然是 11×11第二层 3D 卷积同样 3×3输出空间还是 11×11。真正把空间压缩到 1×1 的其实是最后那层fc的输入尺寸16 * 3 * 1 * 1也就是说中间还隐含了一个自适应池化或裁剪逻辑。实际源码里我一般会在第二个 3D 卷积后接一个AdaptiveAvgPool3d(1)这样无论 patch_size 是 9 还是 13FC 层输入都不用改尺寸。这是一个对新手特别友好的设计因为调 patch_size 时不会连带改模型。BatchNorm 在小样本训练里容易变成埋雷点batch size 太小时BN 统计量不稳定训练震荡明显。如果 batch size 只有 16我建议把BatchNorm3d换成GroupNorm或者干脆去掉。这个取舍会在第 5 章避坑里重讲。3.4 训练循环与评估OA、AA、Kappa 到底怎么算训练循环本身不复杂复杂的是在每轮里同时做三件事计算训练 loss、记录训练精度、在测试集上一轮一算评估指标。源码里常见的做法是把整张图划分成训练索引和测试索引而不是把 patch 直接 random split——因为 random split 会导致同一 patch 的相邻副本同时出现在训练集和测试集里精度虚高。下面这段代码是严格按像素索引划分的from sklearn.metrics import accuracy_score, cohen_kappa_score from sklearn.model_selection import train_test_split import torch.optim as optim # labels 是去掉背景后的标签数组 train_idx, test_idx train_test_split( np.arange(len(labels)), test_size0.7, stratifylabels, random_state42 ) train_labels labels[train_idx] test_labels labels[test_idx] class_counts np.bincount(train_labels) train_weight 1.0 / (class_counts 1e-6) weights torch.tensor(train_weight / train_weight.sum(), dtypetorch.float32) model SmallHybridCNN(in_bandsX_patches.shape[-1], n_classeslen(np.unique(labels))) optimizer optim.Adam(model.parameters(), lr0.001, weight_decay1e-4) loss_fn nn.CrossEntropyLoss(weightweights)这个划分和加权的思路是整套源码里最值得抄的部分。test_size0.7意味着只拿 30% 的样本训练。听起来很浪费但这才是小样本场景的真实手感。stratifylabels保证每个类别在训练集里的占比和全图一致避免某个小类别在训练集里直接消失。CrossEntropyLoss的weight参数则按类别样本数的反比加权少数类贡献更大的梯度避免模型把所有样本都判定为多数类。评估指标分三层OA 是总体精度所有被正确分类的像素除以全部测试像素AA 是先算每个类的召回率再取平均对类别不平衡更敏感Kappa 是排除了随机一致性后的系数。三者对比能看出模型的真实水平。如果 OA 高但 AA 低说明少数类是重灾区。评估代码用 sklearn 一行能出pred model(test_patches).argmax(dim1).cpu().numpy() oa accuracy_score(test_labels, pred) kappa cohen_kappa_score(test_labels, pred) matrix confusion_matrix(test_labels, pred) class_acc matrix.diagonal() / matrix.sum(axis1) aa class_acc.mean() print(fOA{oa:.4f} AA{aa:.4f} Kappa{kappa:.4f})4. 小样本高光谱分类的参数怎么调才不翻车patch 尺寸、降维与训练策略4.1 patch_size 的选择9、11、13 之间的边界效应patch_size 是整个模型里牵一发动全身的参数。patch 太小空间上下文不够模型学不到纹理信息Indian Pines 上 patch_size5 时 OA 普遍比 11 低 3 到 5 个百分点patch 太大一个 patch 里同时包含多种地物边界模型被边界像素弄糊涂等于强行让网络拟合一个混合标签。常见做法是在 9 和 13 之间试11 是默认推荐。另一个容易忽略的是 patch_size 与 3D 卷积时空维度的配合。如果第一层卷积核在光谱维是 7那么 patch 的光谱长度必须大于 7小样本场景下没问题但空间维上patch_size11 经过两次 3D 卷积再加一次 2D 卷积空间特征图尺寸只剩 1×1信息损失可控。如果 patch_size15建议在中间加一层AdaptiveAvgPool强制把空间特征压下来不然 FC 层的输入维度会变得很大参数增加小样本直接过拟合。4.2 波段降维PCA 降维什么时候有用什么时候是负优化高光谱的波段之间高度相关特别是相邻波段。PCA 把 200 个波段降到 30 个主成分等于用 15% 的数据保留绝大部分光谱方差能显著降低模型输入维度和过拟合风险。在 Indian Pines 上PCA 降维到 30 波段后训练速度几乎翻倍OA 下降通常在 1% 以内。但 PCA 也有翻车场景遇到类别之间的光谱差异集中在某些特定窄波段时比如区分两种岩性差异可能只在某个吸收谷波段。PCA 是按全局方差找主成分全局方差大的波段不一定是分类判别力强的波段所以特殊目标检测任务里 PCA 反而会丢关键信息。我的做法是分类任务默认做 PCA 到 30~40 波段如果是矿物精细识别就保留原始波段跑一次对比两边取 OA 高的那个方案。from sklearn.decomposition import PCA pca PCA(n_components30) H, W, B X_norm.shape X_flat X_norm.reshape(-1, B) X_pca pca.fit_transform(X_flat).reshape(H, W, 30)运行 PCA 前有个注意点要把背景像素也一起拿去拟合 PCA否则背景区域的方差分布和前景地物不一致降维后前景光谱会被背景带偏。套用 pca 变换时整张图一起变换不需要排除背景。4.3 学习率、权重衰减与训练轮数的配合小样本条件下模型收敛得快但也特别容易过拟合。常见参数组合是Adam 优化器初始 lr 为 0.001weight_decay 在 1e-4 到 5e-4 之间训练轮数 100 到 200 之间。越小的训练集weight_decay 应该给得越重因为正则化是小样本模型的救命索。训练轮数不能照搬全监督场景的 500 epoch。每类只有 20 个样本时模型大概 30 个 epoch 就能在训练集上到 99% 精度但测试集精度在某个 epoch 达到峰值后会迅速掉头向下。解决方法是加早停每 5 个 epoch 算一次测试集 OA连续 10 次没有提升就停止。下面是源码里常见的早停写法best_oa, best_state, patience 0, None, 10 wait 0 for epoch in range(200): model.train() optimizer.zero_grad() out model(train_patches) loss loss_fn(out, train_labels_t) loss.backward() optimizer.step() model.eval() with torch.no_grad(): logits model(test_patches) pred logits.argmax(dim1).cpu().numpy() oa accuracy_score(test_labels, pred) if oa best_oa: best_oa, best_state oa, model.state_dict().copy() wait 0 else: wait 1 if wait patience: break model.load_state_dict(best_state)这套源码里我会特别强调“保存最优状态”而不是“保存最终 epoch 权重”。测试集精度在训练中期就到达峰值最后 20 个 epoch 反而是在遗忘之前学到的判别特征。用best_state回滚才是小样本训练里真正的后悔药。4.4 样本划分策略随机划分的空间泄漏问题小样本划分策略的重要性不亚于模型结构。最常见的错误是直接在 patch 集合上train_test_split因为 patch 是从整张图上滑窗采样的相邻 patch 大量重叠。如果一个 patch 进了训练集它边缘的像素基本也出现在测试集 patch 里模型测试时看到的邻域范围和训练时几乎一样精度自然高得离谱。正确做法是先按像素坐标划分把整张图的像素点按坐标切出一部分作为测试像素再围绕这些测试像素生成测试 patch。更严格的做法是按地块划分把一幅图分成互不相交的几块矩形区域每个区域的像素全部进训练或全部进测试这样训练 patch 和测试 patch 之间不存在任何空间重叠。第 6 章会进一步讲按地块划分的验证逻辑。5. 避坑指南小样本高光谱分类的 5 个踩坑记录5.1 现象训练 loss 变成 NaN第一个 epoch 就崩原因基本是学习率过大或者输入数据里有 NaN。小样本场景下训练数据本来就不多patch 切完后如果原始 mat 的坏波段没有被剔除某些波段整列为 0归一化时出现除零就会让网络参数发散。另外标签为 0 的背景如果没被过滤送到 CrossEntropyLoss 时类别索引为 0模型会努力把背景学成一类产生梯度冲突。解决先检查数据里是否有 NaN 和 inf对每个波段做 min-max 时加eps防止除零再确认标签过滤逻辑背景像素不生成 patch最后把初始 lr 降到 0.0005训练一个 epoch 看 loss 是否稳定下降。5.2 现象训练精度 99%测试精度只有 65%这是典型的空间泄漏。原因前面已经反复说过随机划分 patch 时同一像素邻域同时出现在训练和测试集里模型在测试时看到了“见过一半的邻居”等于作弊。另一个可能是数据集本身类别极度不平衡Indian Pines 的 Soybean-min 类样本极少模型把这一整类全部预测成多数类测试集精度立刻被拉低。解决严格按像素坐标分层采样不要用随机索引打乱 patch观察相同模型在不同数据集的表现差异如果 Pavia 虚高但 Indian Pines 崩了重点检查类别数、每类样本数是否均衡再用weight参数调整类别权重。5.3 现象Indian Pines 跑到 97%换到 Pavia University 直接只有 70%主要原因有两层。第一Indian Pines 空间尺寸小、地物分布相对集中patch 采样时很容易产生高重叠精度虚高的成分更多。Pavia 空间尺寸大、地物边界复杂模型学到的空间纹理不完全通用。第二两个数据的预处理不一样Indian Pines 有标准吸水带去噪列表Pavia 没有统一列表不同源码用不同的波段保留策略最后模型输入波段数量都不一样直接比 OA 没有意义。解决统一预处理流程至少保证两个数据集都用同一套归一化、同一套 PCA 配置、同一个 patch_size。如果是在自己数据集上做迁移测试建议先统计目标数据集的波段范围与类别分布不要直接拿预训练权重硬灌。5.4 现象显存溢出patch_size 从 11 调到 13 就 OOM显存爆掉的核心原因是 3D 卷积中间张量太大。patch_size13 再加上 16 个特征图4D 张量在 GPU 上占用的显存比小 patch 成倍增长。但模型参数本身并不大真正吃显存的是每个样本的中间特征图。解决把 batch_size 从 64 降到 16或者把torch.utils.data.DataLoader的num_workers调到 0 减少内存峰值。更有效的办法是在第一个 3D 卷积后立刻做一次空间池化把 13×13 压到 7×7后面再卷积就不会爆。也可以把输入从 float64 转成 float32显存消耗直接减半。5.5 现象Kappa 为负预测结果还不如瞎猜Kappa 为负意味着模型预测结果与真实标签的一致性比随机还差通常发生在极端类别不平衡加上错误的类别加权下。如果训练集中某个类别只有 2 个样本而模型输出层把它与另一个类别完全混淆就会大幅拉低 Kappa。更常见的原因是测试集里包含大量未参与训练的类别标签映射错位。解决先输出混淆矩阵看哪些类别相互混淆对样本数小于 10 的类别在训练时用复制增强把这个类别的 patch 重复多遍或者干脆在评估时剔除无法训练的类别。小样本场景下不必强求 16 类全保去掉那些连专家都分不清的类别得到的 Kappa 才有参考价值。6. 往小样本方向再进一步用跨数据集验证模型的真实泛化能力6.1 用随机像素划分冒充小样本为什么测试精度虚高很多人跑完这套源码OA 在 96% 以上以为模型已经成熟换到真实场景却完全不对。问题几乎都出在划分方式上随机像素划分天然带着空间重叠模型没有遇到真正“陌生”的邻域。更可靠的做法是把图像按坐标分成几块不相交的区域比如 Indian Pines 的 145×145 图像左上角 72×72 做训练右下角区域做测试保证训练和测试的地块完全分离。这种划分下 OA 通常会从 96% 掉到 82%~88%这才是小样本模型的真实水平。6.2 真正的落地验证按地块划分训练集再加 t-SNE 可视化我在复现类似方案时最后一步一定是做两个验证一是按地块划分的跨区域测试二是把 model 提取的特征用 t-SNE 降维投影看不同类别在特征空间里是否真正分离。特征可视化能帮你发现模型有没有偷懒如果 t-SNE 图里不同类别明显分层说明模型学到了光谱-空间特征如果混成一团但测试精度很高说明测试集划分存在泄漏需要重新设计。如果追求更远的进阶方向小样本场景下还可以试对比学习预训练先用无标签像素做 patch 级对比学习再用少量标签微调分类头。我在自用数据集上做过实验每类 5 个样本时对比学习预训练比直接监督训练提升 4~7 个百分点的 OA。这套源码目前还没涉及对比学习但你可以把第 3 章的模型作为特征提取器自己接一个 SimCLR 或 BYOL 的投影头算是一条很顺的扩展路径。现在回头看我在这类项目里最深的教训就是不在单个数据集上调参到极限而是先按地块划分验证模型泛化再回头微调参数。这个小习惯帮你守住精度底线也避免被虚高的 OA 骗掉时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表