简介:基于Python实现的高光谱遥感影像识别与分类项目,面向毕业设计、课程设计与项目开发场景,适合需要完成遥感图像分类课题的学生或研究者。资源围绕高光谱数据“休斯现象”展开,提出了基于波段组合(2D)2PCA的降维方法,并构建双通道卷积神经网络提取空谱特征,最后融合CNN-SVM提升泛化能力,形成完整的识别与分类方案。压缩包共24个文件、约6.01MB,包含13个Python源码文件、2个MAT数据文件、PNG图例、pyc缓存以及Markdown/TXT文档,源码经测试可直接参考并扩展使用。已有199人学习,项目中含分类模型、数据预处理、日志记录等模块,配套文档能帮助快速理解算法流程与工程实现。适合作为高光谱遥感分类的参考实现,既可支撑论文实验对比,也可用于课程汇报或二次开发。
1. 高光谱遥感影像识别与分类:从高维像素到分类产品,这条路怎么走
做高光谱遥感影像分类,第一道坎不是模型,而是“数据怎么读进来、波段怎么处理、标签怎么对齐”。一张高光谱影像里每个像素带着几十到几百个波段,信息量远超普通 RGB 图,但标注样本往往只有几千甚至几百个,这种“高维度、小样本”的矛盾,决定了你没法套用常规图像分类套路。这个标题要解决的,正是从原始遥感影像到最终分类图的完整闭环:数据读入、预处理、降维、模型训练、精度评估和结果可视化。
这篇笔记适合正在做毕业设计、课程设计或项目开发的同学。我的目标是让你照着下面的代码和思路,把 Indian Pines 或 Pavia University 这类公开数据集跑通,并且搞明白每一个参数为什么这么设、每一步失败时该看什么。高光谱分类不是把图片喂进 CNN 那么简单,它更像一个“先理解数据分布,再选择模型复杂度”的工程问题。
2. 先看数据长什么样:高光谱数据的结构、读入方式与标签分布
2.1 主流公开数据集的差异决定了你的方案上限
准备动手之前,先选一个合适的基准数据集。做高光谱分类的毕业设计,99% 的人会从 Indian Pines、Pavia University 和 Salinas 这三个公开数据集里选一个起步。
| 数据集 | 空间尺寸 | 波段数 | 类别数 | 特点与适用场景 |
|---|---|---|---|---|
| Indian Pines | 145 × 145 | 200(去除吸水波段后) | 16 | 数据小、跑得快,适合验证模型流程,是多数论文的基准 |
| Pavia University | 610 × 340 | 103 | 9 | 空间分辨率较高,适合做细节纹理和边缘保持的实验 |
| Salinas | 512 × 217 | 204 | 16 | 地物边界清晰,类别多,适合做分类精度对比 |
我一般会让刚开始接触高光谱的同学先从 Indian Pines 入手,因为它的空间尺寸小,哪怕是只跑 SVM 也能在几分钟内出结果。Pavia University 更适合做“空间上下文到底有没有用”这类对比实验,因为它的地物边缘更细碎。Salinas 的类别多,对类别不平衡的挑战更明显。
这里要提醒一个容易忽略的点:这些数据的波段数并不是原始全部波段。Indian Pines 原始有 220 个波段,但包含水吸收和噪声波段,公开的 corrected 版本通常保留 200 个。读入数据后,第一步一定是确认数组的形状,别想当然。
2.2 从 .mat 和 .tif 读入到 numpy:读取代码与波段顺序的坑
公开的高光谱数据集常见存储格式有 .mat、.tif 和 .npy。Indian Pines 和 Salinas 常用 .mat 格式,Pavia University 有时以 .tif 或 .mat 发布。实现的第一步,是把它们统一读成 numpy 数组。
python import numpy as np import scipy.io as sio from osgeo import gdal # 若数据是 .tif 格式
def load_hsi_mat(mat_path, data_key): """ 读取 .mat 格式的高光谱数据 mat_path: 文件路径 data_key: 数据在 mat 文件里的变量名,常见为 'indian_pines_corrected' 等 """ mat = sio.loadmat(mat_path) data = mat[data_key].astype(np.float32) # 高光谱数据一般是 uint16,转 float32 便于计算 return data
def load_hsi_tif(tif_path): """ 读取 .tif 格式的高光谱数据 返回 (height, width, bands) 顺序的数组 """ ds = gdal.Open(tif_path) bands = ds.RasterCount # GDAL 读出来是 (bands, height, width),需要转置成 (height, width, bands) arr = np.array([ds.GetRasterBand(i + 1).ReadAsArray() for i in range(bands)]) arr = np.transpose(arr, (1, 2, 0)).astype(np.float32) return arr
逻辑说明:scipy.io.loadmat读取 .mat 文件后返回一个字典,需要用数据对应的 key 取值;gdal.Open读取 .tif 时,默认返回的是 (波段数, 高, 宽) 的维度顺序,图像处理里更常用 (高, 宽, 波段),所以这里显式做 transpose。
参数说明:data_key写成参数而不是硬编码,是因为不同版本的 mat 文件里变量名不一样,常见的有indian_pines_corrected、paviaU、salinas_corrected。如果读取时报 KeyError,先print(mat.keys())看真实键名。
这里要特别说一个非常隐蔽的坑:遥感影像的波段存储顺序有 BIP、BIL、BSQ 三种。BIP(波段按像素交替存储)和 BIL(按行存储)常见于 ENVI 导出的数据,BSQ 则是每个波段完整存储。用 GDAL 读 .tif 一般会自动处理,但如果你拿到的是原始二进制文件(比如 .raw 或 .dat),就必须知道它的存储顺序,否则读出来的数组维度对不上,训练出来的模型精度会异常低,而且很难排查。
2.3 标签不平衡:训练前必须统计每个类别的样本量
高光谱分类里最容易被忽略的一步是查看标签分布。Indian Pines 的 16 个类别里,有的类别只有 20 个样本,有的类别超过 1000 个。如果不做任何处理,模型会把多数类学得很好,少数类直接忽略,整体精度看起来很高,但个别类别精度惨不忍睹。
python def show_class_distribution(gt, class_names=None): """ gt: 标签矩阵,形状为 (height, width),背景像素值为 0 打印每个类别的样本数量 """ values, counts = np.unique(gt, return_counts=True) for v, c in zip(values, counts): if v == 0: print(f"背景像素: {c}") else: name = class_names[v - 1] if class_names else "class_" + str(v) print(f"{name}: {c} 个像素")
假设 gt 是 145x145 的标签矩阵
show_class_distribution(gt, class_names=['Corn-notill', 'Corn-mintill', ...])
逻辑说明:np.unique同时返回标签值和对应的像素数量,背景像素(值为 0)是未标注区域,不能参与训练。先打印分布,你才能决定用分层采样还是样本加权。
参数说明:这里标签值从 1 开始编号,0 统一表示背景,这是 Indian Pines 等数据集的约定。如果你用的数据集不是这个约定,需要先做一次标签重映射,统一成 0=背景、1~N=类别。
统计完分布后,常见的处理方式有两种:一是采用分层抽样(train_test_split里设置stratify=y),保证训练集和测试集每个类别的比例一致;二是给少数类更高的类别权重,在sklearn里对应class_weight='balanced'。这两种方式不冲突,建议都做。
3. 模型怎么选:从 RBF-SVM 到 3D-CNN 的完整路径
3.1 经典 RBF-SVM 为什么仍是最可靠的 baseline
很多同学一上来就想用深度学习,但高光谱数据的维度高、样本少,直接上深度模型很容易过拟合。RBF-SVM 在高光谱分类里被用了二十年,至今仍是论文里的标准对比方法,原因有两个:一是 SVM 对高维小样本数据有很好的泛化能力,二是 RBF 核可以隐式地把光谱向量映射到高维空间,处理非线性分类问题。
RBF-SVM 的核心参数只有两个:C(正则化系数)和gamma(核函数宽度)。C越大越容易过拟合,gamma越大决策边界越复杂。高光谱里常见的做法是C=[1, 10, 100, 1000]和gamma=[0.01, 0.001, 0.0001]做网格搜索,但千万别上来就在全波段上跑,一定要先做标准化和降维。
3.2 用 sklearn 实现像素级 SVM 分类:标准化、降维与训练
下面给出一段可以直接跑通的完整代码。这里采用的方案是:按像素把高光谱三维数组展开成二维(样本数 × 波段数),标准化后用 PCA 降到 30 维,再训练 RBF-SVM。
python from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA from sklearn.svm import SVC from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score, cohen_kappa_score
def train_svm_classifier(data, gt, n_components=30, test_size=0.3, random_state=42): """ data: (height, width, bands) 的高光谱影像 gt: (height, width) 的标签矩阵,背景为 0 """ h, w, b = data.shape # 展开像素:每一行是一个像素的光谱向量 X = data.reshape(h * w, b) y = gt.reshape(h * w)
# 只保留有标签的像素 mask = y > 0 X, y = X[mask], y[mask] # 标准化 scaler = StandardScaler().fit(X) X_scaled = scaler.transform(X) # PCA 降维 pca = PCA(n_components=n_components).fit(X_scaled) X_pca = pca.transform(X_scaled) # 分层划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split( X_pca, y, test_size=test_size, stratify=y, random_state=random_state ) # RBF-SVM clf = SVC(C=100, gamma=0.001, kernel='rbf', class_weight='balanced') clf.fit(X_train, y_train) y_pred = clf.predict(X_test) print(f"OA: {accuracy_score(y_test, y_pred):.4f}") print(f"Kappa: {cohen_kappa_score(y_test, y_pred):.4f}") return clf, scaler, pca逻辑说明:先把三维数组reshape成二维,让每个像素变成一个样本。提取有标签的像素后,标准化的fit只作用在所有有标签像素上,train_test_split采用stratify=y保证每个类别的训练测试比例一致。class_weight='balanced'会自动给少数类更高的惩罚权重,缓解类别不平衡。
参数说明:n_components=30是一个比较稳妥的起点,Indian Pines 降到 30 维通常能保留 95% 以上的方差。C=100、gamma=0.001是经验值,不同数据集上最好用GridSearchCV搜索,但搜索前先固定 PCA 维度,否则搜索空间会爆炸。
这段代码里还有一个常被忽略的点:标准化和 PCA 必须在训练集上完成,然后再transform测试集。我在实操里见过直接把全部数据标准化后再划分的做法,这样测试集的信息已经泄露到训练过程里,精度虚高,评审质疑时很难解释清楚。
3.3 用 PCA 降维后的数据构造 2D-CNN:邻域窗口让每个像素看到周围
SVM 只看单个像素的光谱曲线,没有使用空间上下文。高光谱影像里相邻像素通常是同一类地物,所以引入邻域信息能稳定提升精度。常见做法是先对影像做 PCA 降维(通常降到 3~5 个主成分),然后以每个像素为中心取一个大小为w × w的窗口,作为 2D-CNN 的输入。
python def extract_patches(data_pca, gt, patch_size=9): """ data_pca: (height, width, n_components) 降维后的影像 gt: (height, width) 标签矩阵 patch_size: 窗口大小,奇数 返回:每个有标签像素对应的邻域窗口 """ h, w, c = data_pca.shape pad = patch_size // 2 # 边界补零,保证边缘像素也有完整窗口 data_padded = np.pad(data_pca, ((pad, pad), (pad, pad), (0, 0)), mode='constant')
X_patches = [] y_labels = [] coords = [] for i in range(h): for j in range(w): if gt[i, j] > 0: patch = data_padded[i:i + patch_size, j:j + patch_size, :] X_patches.append(patch) y_labels.append(gt[i, j]) coords.append((i, j)) X_patches = np.array(X_patches, dtype=np.float32) y_labels = np.array(y_labels, dtype=np.int64) return X_patches, y_labels, coords逻辑说明:对整幅影像做边界补零后,窗口中心(i, j)对应的补丁是data_padded[i:i+patch_size, j:j+patch_size, :],这个切片同时包含光谱维和空间维。patch_size=9表示每个样本是 9×9 的邻域,加上 PCA 的 3 个通道,输入形状是 (9, 9, 3)。
参数说明:patch_size的选择决定了模型能看到的空间范围。窗口太小看不到地物纹理,窗口太大则边界处混入不同类别的像素,反而干扰分类。Indian Pines 上 7×7 到 11×11 都比较常见,Pavia University 地物细节多,5×7 的效果可能更好。注意窗口大小必须是奇数,否则中心像素会偏移。
2D-CNN 的优点是把每个像素从“一条光谱曲线”扩展成“一幅小图”,可以用标准卷积提取空间特征。但代价是降维:如果直接用 200 个波段生成邻域窗口,输入变成 (9, 9, 200),参数量会变得非常恐怖,训练速度慢且极容易过拟合。所以 PCA 降到 3~5 个主成分是常见做法,这是深度方案里“光谱维压缩,空间维展开”的典型结构。
3.4 3D-CNN:让卷积同时扫光谱和空间,但别忽视参数量
2D-CNN 的问题在于,PCA 降维虽然减少了计算量,也丢掉了部分光谱细节。3D-CNN 的思路是不做大幅降维,直接把邻域窗口连同波段维一起送入三维卷积,让卷积核同时沿空间和光谱方向滑动。这样模型能学到光谱维度的局部连续特征,因为相邻波段之间存在相关性,而这种相关性在 2D-CNN 中被 PCA 抹掉了。
3D-CNN 的输入形状通常是 (patch_size, patch_size, n_bands),卷积核也是三维的。下面给出一个极简的 3D-CNN 结构示例,便于理解输入输出尺寸的变化。
python import torch.nn as nn
class Simple3DCNN(nn.Module): definit(self, in_channels=1, n_classes=16): super().init() # 输入形状: (batch, 1, patch, patch, bands) self.conv1 = nn.Conv3d(in_channels, 8, kernel_size=(3, 3, 7), padding=(1, 1, 3)) self.conv2 = nn.Conv3d(8, 16, kernel_size=(3, 3, 5), padding=(1, 1, 2)) self.pool = nn.MaxPool3d(kernel_size=(1, 1, 2)) self.flatten = nn.Flatten()
# 这里需要根据输入尺寸计算展平后的特征数 self.fc = nn.Linear(16 * 7 * 7 * 24, n_classes) # 示意:具体数值要按实际输入计算 def forward(self, x): x = self.conv1(x) x = self.conv2(x) x = self.pool(x) x = self.flatten(x) x = self.fc(x) return x逻辑说明:Conv3d的卷积核是 (空间高, 空间宽, 光谱深度),kernel_size=(3,3,7)表示每次在空间上看 3×3 邻域,在光谱维上跨越 7 个波段。这样设计是因为相邻波段信息高度相关,卷积核不必跨得太宽。
参数说明:这里展平后的 Linear 输入维度是写死的,真实项目里需要用torch.nn.Module的forward传入一个实际张量打印形状,再回填这个数值。这是个非常容易翻车的地方,很多人改patch_size后忘记调fc的输入维数。
3D-CNN 的瓶颈在于显存和训练时间。Indian Pines 如果取 11×11 的窗口、200 个波段,单个样本就有 11×11×200=24200 个数值,batch size 一大显卡就会爆。实际项目中可以先对 200 个波段做 PCA 降到 30~50 维,再送入 3D-CNN,这样保留的光谱信息比 PCA 到 3 维多得多,同时显存压力可控。这个“PCA 到中间维度 + 3D-CNN 提特征”的混合方案,是我做高光谱分类时最常推荐给学生的路线。
4. 精度评估与结果可视化:OA、AA、Kappa 和分类图的完整输出
4.1 OA、AA、Kappa 分别度量什么,为什么一个都不能省
高光谱分类的论文里,精度评估有三个指标是标配:OA(Overall Accuracy)、AA(Average Accuracy)和 Kappa 系数。很多刚入门的同学只报一个 OA,答辩时被问“你这模型对每一类分别怎么样”就卡住了。
- OA:所有测试样本里分类正确的比例,反映整体正确率。
- AA:先对每个类分别计算分类精度,再对所有类取平均。它不受类别样本量影响,能把“小类全错但 OA 很高”的问题暴露出来。
- Kappa:衡量分类结果与随机分类相比的改善程度,取值范围 -1 到 1,大于 0.8 表示分类结果与真值高度一致。
在 Python 里这三个指标的计算用 sklearn 一行就能完成:
python from sklearn.metrics import accuracy_score, cohen_kappa_score, classification_report
def evaluate_model(y_test, y_pred, class_names=None): oa = accuracy_score(y_test, y_pred) kappa = cohen_kappa_score(y_test, y_pred)
# classification_report 会输出每个类别的 precision/recall/f1 report = classification_report(y_test, y_pred, target_names=class_names, digits=4) # AA 的计算方式:各类别 recall 的平均 from sklearn.metrics import recall_score recalls = recall_score(y_test, y_pred, average=None) aa = recalls[recalls > 0].mean() # 某个类别可能没有样本落在测试集,需要过滤 print(f"OA: {oa:.4f}") print(f"AA: {aa:.4f}") print(f"Kappa: {kappa:.4f}") print(report) return oa, aa, kappa逻辑说明:classification_report的好处是能直接看到每个类别的 precision、recall 和 f1,这是论文实验表格里最常贴的数据。AA 在 sklearn 里没有直接函数,用recall_score(average=None)拿到每个类的 recall 再求平均即可。
参数说明:average=None返回一个数组,元素个数等于类别数。要注意测试集里如果某个类完全没有样本,np.nan会被过滤掉后再求平均。实际实验里为了公平,训练测试划分时最好保证每个类至少有一定数量的测试样本,否则 AA 会出现波动。
4.2 训练测试集划分方式:随机划分与分块划分的结果差距有多大
高光谱分类的精度报告里,训练测试集的划分方式直接决定结果是否可信。学术界对这个问题已经吵了很多年,核心矛盾在于:遥感影像相邻像素高度相关,随机像素划分会让训练集和测试集里出现大量空间邻近的像素,模型等于“看到过”测试样本的邻居,精度自然虚高。
常见划分方式有四种:
| 划分方式 | 做法 | 适用场景 | 精度特征 |
|---|---|---|---|
| 随机像素划分 | 所有有标签像素中随机抽 70% 训练、30% 测试 | 快速验证模型流程 | 偏高,特别是大斑块类别 |
| 分层随机划分 | 按类别比例随机抽取,保证各类数量均衡 | 常规对比实验 | 相对可信,但仍有空间相关性 |
| 分块划分 | 把影像切块,部分块训练、部分块测试 | 检验模型泛化能力 | 精度明显下降,更接近真实场景 |
| 按地块划分 | 按实际地物地块边界切分 | 农田/城市制图任务 | 最严格,也最难达到高精度 |
这块的落地建议是:如果是做课程设计,用分层随机划分就够;如果是做毕业论文或要发表的结果,至少补一个分块划分的对照组。操作方法是用sklearn.model_selection.KFold按像素坐标的块号分组,而不是直接对样本数组切分。
4.3 生成分类结果图:从预测标签到论文配图
分类实验做完,最终交付物必须是一张分类结果图。它和普通图片分类不同,不是输出一张图的类别,而是把每个像素的预测类别映射回原始影像的空间位置,形成一幅语义分割图。
python import matplotlib.pyplot as plt import numpy as np
def save_classification_map(pred, gt, save_path): """ pred: 预测标签,长度等于有标签像素数量 gt: (height, width) 的标签矩阵,用于还原空间位置 save_path: 输出图片路径 """ h, w = gt.shape # 创建空白的分类图,0 表示背景 class_map = np.zeros((h, w), dtype=np.int64)
mask = gt > 0 # 按有标签像素出现的顺序把 pred 填回空间位置 class_map[mask] = pred # 使用离散颜色映射,避免连续渐变色带来的视觉误导 cmap = plt.cm.tab20 # 20 种离散颜色,适合 16 类地物 plt.figure(figsize=(6, 6)) plt.imshow(class_map, cmap=cmap, interpolation='nearest') plt.colorbar(shrink=0.8) plt.axis('off') plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.close()逻辑说明:这里的关键是class_map[gt > 0] = pred这一步。之前提取训练样本时是按行扫描、只取有标签像素,预测结果的顺序与扫描顺序一致,所以这里直接按相同顺序回填即可。最怕的就是训练代码里用了随机采样打乱顺序,又没有保留索引对应关系,导致图和真值错位。
参数说明:cmap=plt.cm.tab20的离散色带最多支持 20 类,足够覆盖 Indian Pines 的 16 类。dpi=300满足论文图片的基本要求,interpolation='nearest'避免插值把类别的边界抹模糊。
生成分类图之后,建议把原图、真值图、预测图三张图并排放在一起做目视对比。高光谱分类评审时,除了数值指标,地物边界的完整性、斑块内部的噪点数量都是重要评价点,一张干净的分类图比一个虚高的 OA 更能说服人。
5. 高光谱分类避坑指南:5 个让模型翻车的实操问题
5.1 随机划分训练测试集,精度刷到 98%,但评审不认
这是我见过翻车最多的地方。有不少公开源码包按像素随机划分,Indian Pines 上 OA 轻松刷到 98% 以上,但仔细分析会发现,训练集和测试集里大量像素来自同一个地物斑块,空间位置几乎重叠,模型相当于开了“上帝视角”。
解决方法是把精度分成两档来汇报:第一档用分层随机划分,数据量和大部分论文可比;第二档用分块划分或按地块划分,直接体现模型的真实泛化能力。后者的精度通常会下降 5~15 个百分点,但这份结果才是能写进论文且经得起复现推敲的。
5.2 没做标准化就直接训练 SVM,前几个波段主导预测
高光谱不同波段的量纲差异很大,有些波段反射率集中在 0~0.5,有些波段均值接近 1000,如果不做标准化,SVM 的 RBF 核计算距离时会被数值大的波段完全主导,模型训练时看似收敛,实际学到的是噪声。
解决方法是先StandardScaler再训练,但要注意只对有标签像素做fit,然后对整幅影像做transform。千万别先对全部数据标准化再提取标签样本,这会造成数据泄露,评估结果不可信。
5.3 全波段直接进入卷积网络,显存爆炸还容易过拟合
很多人第一次用 PyTorch 做高光谱分类,直接把 200 个波段和 11×11 的窗口拼成输入,结果 batch size 设为 16 就显存溢出,或者勉强能跑但验证集精度始终上不去。
原因很简单:200 个波段里有大量冗余信息,相邻波段相关性极高,模型要学习的参数量远超训练样本数。解决方法是先 PCA 降维,Indian Pines 降到 30~50 维通常能保留 99% 以上的方差,既是安全的中间值,也不会丢失光谱细节。如果还想进一步压缩,可以用波段选择算法选出有代表性的波段子集,但这在毕设阶段不是必需品。
5.4 少数类别样本只有十几个,训练出来的模型把它识别成背景
Indian Pines 里的某些类别,比如 Grass-pasture-mowed 和 Oats,标注像素只有几十个甚至十几个。训练集划分后,这类样本可能只剩个位数,模型根本学不到特征,预测时全部归为多数类。
这个问题的解法分两个层面:数据层面用分层采样保证每个类别在训练集里都有一定数量,无法保证时考虑对少数类做数据增强(对窗口做翻转、旋转、加高斯噪声);模型层面用class_weight='balanced'或 PyTorch 里的CrossEntropyLoss(weight=...)手动放大少数类的损失权重。最稳妥的做法是在论文里明确说明少数类精度偏低的原因,而不是硬着头皮报一个虚高的平均值。
5.5 分类图与真值图错位,细节看起来像“鬼影”
生成分类图时最容易出现的问题是:训练时样本顺序经过多次shuffle和索引提取,测试时又按另一套顺序预测,最后把预测结果填回空间位置时张冠李戴,分类图上的地物边界和原图完全对不上。
解决方法是严格保持索引一致性。我的习惯是训练前先记录每个有标签像素的行列坐标,训练和预测都按照这个坐标列表的顺序进行,最后直接用坐标表逐像素填图。任何np.random.permutation或DataLoader的shuffle=True都只作用于训练阶段,测试阶段必须关闭洗牌,否则坐标对应关系就断了。
6. 进阶技巧:训练日志、模型保存与可复现工程结构
做高阶实验时,我习惯把一个完整的毕设项目按固定结构组织。这不是形式主义,而是每个文件有明确职责,换机器重跑、改参数、出结果都只需要动一个入口。
hyperspectral_classification/ ├── data/ # 原始数据集,只读,不修改 │ ├── indian_pines/ │ │ ├── indian_pines_corrected.mat │ │ └── indian_pines_gt.mat ├── src/ # 核心代码 │ ├── data_loader.py # 数据读入与预处理 │ ├── feature_extract.py # PCA降维、波段选择 │ ├── models/ │ │ ├── svm_model.py │ │ └── cnn_model.py │ ├── train.py # 训练入口 │ └── evaluate.py # 指标计算与可视化 ├── docs/ # 项目文档与说明 │ ├── 环境配置说明.md │ └── 运行步骤.md ├── output/ # 实验结果 │ ├── logs/ # 训练日志 │ ├── models/ # 保存的模型权重 │ └── figures/ # 分类结果图 └── requirements.txt这种组织方式的核心价值是可复现。我以前吃过亏:实验做完了,但忘记记录当时用的C和gamma是多少,重跑时怎么都复现不了原始精度。后来我养成一个习惯——把每次实验的关键参数和对应指标追加到一个experiment_log.csv文件里,文件只有四列:timestamp, model, params, oa。这个习惯在毕设和项目开发中至少能帮你省下两天的返工时间。
模型训练时,另一个值得做的技巧是“只保存验证集上最优的模型”。比如 PyTorch 训练时,在每个 epoch 结束后算一算验证集 OA,如果高于历史最高值就保存权重,否则不覆盖。这套逻辑叫“early stopping + best model checkpoint”,比训练几个 epoch 后随便保存一份权重要稳妥得多。
到最后,整个项目能不能交付,看三样东西:代码能不能从空环境一键跑通、文档里有没有把每个文件的输入输出写清楚、分类结果图能不能和真值图对得上。技术方案会更新,但这三个底线不会变。希望这篇笔记能帮你少踩几个坑,把高光谱影像分类这条路走得顺利一些。
本文还有配套的精品资源,点击获取