ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

PyTorch实现Indian Pines高光谱图像分类:从数据预处理到CNN模型实战

2026/10/3 7:59:53 拓冰建站 浏览量
PyTorch实现Indian Pines高光谱图像分类:从数据预处理到CNN模型实战 第一次拿到Indian Pines数据的人十有八九会对着那个145×145的灰度影像发懵整幅图灰蒙蒙一片地面真值图看起来更像是一张随机撒满彩色芝麻的纸。论文里那些五颜六色、边界整齐的高光谱分类图到底是怎么从这个数据集里变出来的答案就是这一句话用PyTorch搭一个卷积神经网络CNN逐像素地学习光谱和空间特征然后输出16类地物标签。Indian Pines被不少人叫作“遥感界的MNIST”因为它公开、规模小、类别清晰特别适合作为CNN处理遥感图像的第一站。这篇文章把你从数据加载一路带到分类结果图包括每个步骤为什么要这么做以及我在实际跑实验时踩过的坑。代码基于PyTorch模型不需要GPU也能训练但我会把GPU环境的配置也一并讲清楚。1. 先认识数据145×145的高光谱影像里到底装了什么1.1 高光谱图像和普通照片本质差异普通相机拍出来的是红、绿、蓝三个波段高光谱传感器则是把0.4到2.45微米的电磁波谱切成上百个窄波段逐一记录地表反射率。Indian Pines原始数据有220个波段去掉水汽吸收严重的波段后常用的是200个。你可以把每个波段想象成同一块地面在不同“颜色”下的照片把所有波段叠在一起就得到一个145×145×220的三维数据立方体。这200个波段的价值在于不同地物有自己独特的光谱反射曲线。以农业区为例大豆和玉米在RGB照片上可能长得非常像可到了某些红外波段两者的反射率差异会非常明显这正是高光谱遥感能做精细作物分类的原因。Indian Pines是1992年由AVIRIS传感器在美国印第安纳州西北部采集的空间分辨率约20米场景覆盖大约6英里见方的农田、林地和建筑区。继续往下做之前先把“数据立方体”这个概念装进脑子里后面所有的预处理都是围绕它展开的。1.2 16类标签的分布一场典型的不均衡现场数据包里的地面真值ground truth是一个145×145的整型矩阵像素值为0表示没有标注1到16对应16类地物。具体分布如下类别编号类别名称样本数1Alfalfa462Corn-notill14283Corn-mintill8304Corn2375Grass-pasture4836Grass-trees7307Grass-pasture-mowed288Hay-windrowed4789Oats2010Soybean-notill97211Soybean-mintill245512Soybean-clean59313Wheat20514Woods126515Buildings-Grass-Trees-Drives38616Stone-Steel-Towers93合计10249总共只有10249个有标注的像素而整幅图像是21025个像素超过一半是背景。最少的Oats只有20个样本最多的Soybean-mintill有2455个两者相差超过100倍。这种“天然不均衡”决定了后面训练时不能简单地用普通交叉熵损失硬train不然后面会专门讲怎么处理。1.3 别把整幅图直接喂给CNNpatch输入、像素级标签新手最容易犯的错误是试图把145×145的图像直接输入CNN做“整图分类”。这行不通原因有两个第一整个数据集里能作为监督信号的只有10249个像素的标注如果不分块图像级分类任务等于只有一个样本第二Indian Pines的标准任务本质上是逐像素分类也就是对每个像素判断它属于哪一类地物。但逐像素分类不等于把单个像素的光谱向量丢进全连接网络。单像素光谱受噪声和混合像元影响很大CNN的价值恰恰在于能同时看中心像素周围一个邻域的空间信息。所以主流做法是以每个有标注的像素为中心裁出一个固定大小的小方块patch比如11×11×C网络输入是这个小方块标签是中心像素的类别。这就是“patch输入、像素级标签”的基本范式。Indian Pines尺寸小裁完所有有标注像素也就一万多个样本正好适合一个小型CNN练手。2. 环境配置把PyTorch装对比选模型更重要2.1 虚拟环境与最小依赖很多人在模型跑不动的时候才回头查环境实际上环境问题在遥感图像分类项目里非常常见。我的习惯是用Anaconda建一个独立虚拟环境Python版本选3.9兼容性最稳。conda create -n hsi python3.9 -y conda activate hsi pip install numpy scipy scikit-learn matplotlib tqdm注意我故意没有在这一步安装torchvision和torchaudio。Indian Pines分类用不到预训练图像模型也不需要音频处理模块装得越少以后依赖冲突的概率越低。torchvision只有在你后面想用ResNet等预训练backbone时才需要。另外scipy和scikit-learn是必须的scipy负责读.mat文件scikit-learn负责PCA、样本划分和评估指标这些都是后续步骤的地基。2.2 官方源安装PyTorchGPU与CPU的取舍PyTorch安装是热搜词里出现频率最高的问题这里说清楚。先确认你的机器有没有NVIDIA显卡在终端执行nvidia-smi能看到显卡信息和驱动版本就是有。有显卡就装GPU版pip install torch --index-url https://download.pytorch.org/whl/cu118没有显卡或者只想先跑通流程装CPU版pip install torch --index-url https://download.pytorch.org/whl/cpu为什么不推荐conda install pytorch我踩过多次坑conda的依赖解析很慢而且在某些机器上会静默装成CPU版本等训练时才发现torch.cuda.is_available()一直是False浪费一晚上。用PyTorch官方wheel源直接指定版本号结果可控得多。cu118对应CUDA 11.8显卡驱动较新的话也可以换cu121或cu124核心原则是你的驱动版本必须能兼容对应的CUDA运行时。装完立刻验证别等代码写完才发现问题python -c import torch; print(torch.__version__, torch.cuda.is_available())输出里True意味着GPU可用。就算输出是False也完全能继续这个项目——我们后面要训练的模型只有约10万个参数用CPU跑几十个epoch一样能出结果只是全图推理时慢一些。2.3 工程目录与数据文件组织一个干净的项目结构能避免很多混乱。我建议这样组织hsi_cnn/ ├── data/ │ ├── Indian_pines_corrected.mat │ └── Indian_pines_gt.mat ├── main.py ├── model.py └── utils.py数据文件从公开渠道下载注意一定要下载Indian_pines_corrected.mat和Indian_pines_gt.mat这两个文件。前者是辐射校正后的高光谱数据后者是地面真值标签。之前有朋友下载了原始未校正的Indian_pines.mat虽然也能用但噪声更大很多论文结果都对不上。3. 预处理决定成败的往往是数据准备3.1 读取mat数据并认清三个基本形状预处理是整个项目里最容易被低估的环节。模型结构可以抄参数可以调但数据格式一旦搞错后面全是白忙。先用scipy读入数据import numpy as np from scipy.io import loadmat img loadmat(data/Indian_pines_corrected.mat)[indian_pines_corrected] gt loadmat(data/Indian_pines_gt.mat)[indian_pines_gt] print(img.shape, img.dtype) # (145, 145, 220) uint16 print(gt.shape, gt.dtype) # (145, 145) uint8这里要建立三个关键形状的概念高光谱数据是(高, 宽, 波段数)地面真值是(高, 宽)类别编号从1到16。dtype是uint16说明原始数据是未经缩放的辐射值范围可能到几千甚至上万所以归一化是必须的。3.2 去掉噪声波段再做逐波段归一化高光谱数据里有一些波段受大气水汽吸收影响反射率信号基本被噪声淹没。Indian Pines最常用的做法是去掉104到108波段、150到163波段以及第220波段共20个波段剩下200个bands_to_remove list(range(103, 108)) list(range(149, 163)) [219] img np.delete(img, bands_to_remove, axis2) print(img.shape) # (145, 145, 200)这里用的是0基索引和论文里常说的1基索引对应关系要小心。去完噪声波段接下来做归一化。我的选择是对每个波段分别做min-max归一化而不是整幅图像统一归一化。原因是不同波段之间的辐射值范围差异很大统一归一化会把本来有用的相对关系压扁。img (img - img.min(axis(0, 1), keepdimsTrue)) / ( img.max(axis(0, 1), keepdimsTrue) - img.min(axis(0, 1), keepdimsTrue) )keepdimsTrue这里特别重要少了它广播维度对不上结果会变成一堆NaN这种错误完全没有报错信息特别难排查。3.3 PCA降维给2D-CNN铺路的常规操作现在数据还是200个波段。如果直接用2D-CNN处理输入就是145×145×200第一层卷积的计算量很大而且这200个波段之间存在大量冗余。PCA降维是这个任务里最常见的选择把200个波段压缩成30个主成分from sklearn.decomposition import PCA H, W, B img.shape pca PCA(n_components30) img_pca pca.fit_transform(img.reshape(-1, B)).reshape(H, W, -1) print(img_pca.shape) # (145, 145, 30) print(pca.explained_variance_ratio_.sum()) # 通常超过0.99实测下来30个主成分通常能保留99%以上的方差信息损失可以忽略。这里有一个严肃的学术问题严格来说PCA应该只在训练集像素上fit再transform验证集和测试集否则训练时就用到了全图的光谱统计信息属于轻微的数据泄漏。教程代码为了方便在整幅图上直接fit了结果会略微偏乐观做科研时请按严格流程来。这个问题后面还会再提。3.4 取patch与通道维度整理预处理最关键的一步是构造训练样本。以每个有标注的像素为中心裁出11×11的邻域所有波段一起保留def create_patches(img, gt, patch_size11): half patch_size // 2 H, W, C img.shape padded np.pad(img, ((half, half), (half, half), (0, 0)), modereflect) X, y [], [] rows, cols np.where(gt 0) for r, c in zip(rows, cols): patch padded[r:r patch_size, c:c patch_size, :] X.append(patch) y.append(gt[r, c] - 1) # 标签从0开始 return np.stack(X), np.array(y) X, y create_patches(img_pca, gt, patch_size11) print(X.shape, y.shape) # (10249, 11, 11, 30) (10249,)边界像素的处理用np.pad的reflect模式也就是镜像反射填充。为什么不用zero填充因为零值会被网络当成“真实地物信息”学进去边界像素的分类结果会莫名其妙变差。reflect填充没有这种问题。得到X之后必须做一次转置把通道维度提到第2位X X.transpose(0, 3, 1, 2) print(X.shape) # (10249, 30, 11, 11)PyTorch卷积层的输入格式是(batch, channels, height, width)而原始数据是(height, width, channels)这个transpose漏掉的话模型会直接报维度错误或者更隐蔽地把波段当成长宽训出一个看似正常但毫无意义的模型。4. CNN结构设计先选路线再堆层数4.1 2D-CNN加PCA与3D-CNN怎么选高光谱分类的CNN路线大体分两类。第一类是2D-CNN配合PCA降维也就是前面预处理铺的路把高光谱先压缩成30个通道然后当成普通多通道图像处理。第二类是3D-CNN输入直接保留200个波段用三维卷积同时提取光谱维和空间维的特征代表性工作是HybridSN。两者怎么选我给一个直观的对比方案输入尺寸模型参数量训练速度适用场景2D-CNN PCA11×11×30约10万快入门学习、快速验证3D-CNN11×11×200百万级慢追求指标、研究光谱特性3D-CNN理论上更强大因为它不丢失光谱维信息但它对显存、训练时间的需求也大得多。对于第一次跑Indian Pines的人来说我强烈建议先走2D-CNN路线把整个pipeline跑通再考虑升级到3D-CNN。我见过太多人一上来就复现HybridSN结果环境、显存、训练收敛各种问题叠在一起连一个能跑的benchmark都没留下。4.2 一个轻量CNN的具体设计这个模型是我在多个高光谱实验里反复用过的轻量结构只有约10万个参数CPU上几十秒就能跑一个epochimport torch import torch.nn as nn class HSICNN(nn.Module): def __init__(self, in_channels30, num_classes16): super().__init__() self.features nn.Sequential( nn.Conv2d(in_channels, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), ) self.pool nn.AdaptiveAvgPool2d(1) self.classifier nn.Sequential( nn.Dropout(0.5), nn.Linear(128, num_classes), ) def forward(self, x): x self.features(x) x self.pool(x) x x.flatten(1) return self.classifier(x) model HSICNN() total sum(p.numel() for p in model.parameters()) print(f参数量: {total:,}) # 约103,53611×11的输入经过前两层3×3卷积后尺寸不变MaxPool2d(2)把它变成5×5再经过第三层卷积仍是5×5最后AdaptiveAvgPool2d(1)把空间维压成1得到一个128维向量。这里用AdaptiveAvgPool而不是直接把特征图展平再接全连接好处是输入patch尺寸即使从11改成13或9模型也能正常工作不用改代码。4.3 为什么BatchNorm和Dropout是必需品小数据集上训练CNN最大的敌人是过拟合。Indian Pines只有一万多个样本而模型有10万参数稍微多训几个epoch训练精度就能逼近100%但验证集表现会停滞甚至下降。BatchNorm和Dropout在这里的作用是决定性的。BatchNorm把每层的激活值拉回到均值为0、方差为1的分布。高光谱数据经过PCA之后不同主成分的分量方差差异很大没有BN的深层卷积网络训练起来非常不稳定经常出现loss剧烈震荡。Dropout则是“随机失活”每个batch随机屏蔽一半神经元迫使网络不依赖某几个特定特征这在小样本任务里是性价比最高的正则化手段。我试过把这个Dropout去掉验证集OA从97%跌到94%左右效果非常明显。另外需要注意模型最后没有额外加Softmax层。PyTorch的CrossEntropyLoss内部已经包含了Softmax计算手动加一层Softmax反而会在数值稳定性上吃亏。5. 训练流程样本划分、类别权重、早停与复现5.1 分层划分训练验证测试集但要警惕空间相关性数据准备好了接下来划分。Indian Pines的标准做法是按像素随机划分但必须用分层采样保证每个类别在训练集、验证集、测试集中的比例一致from sklearn.model_selection import train_test_split X_train, X_tmp, y_train, y_tmp train_test_split( X, y, test_size0.3, stratifyy, random_state42) X_val, X_test, y_val, y_test train_test_split( X_tmp, y_tmp, test_size0.5, stratifyy_tmp, random_state42) print(X_train.shape, X_val.shape, X_test.shape) # (7174, 30, 11, 11) (1537, 30, 11, 11) (1538, 30, 11, 11)这里我要说一个很多人不知道的坑随机划分会让相邻像素的patch在训练集和测试集里同时出现。Indian Pines里的地物大多是连片的农田同一个地块的相邻像素光谱非常相似这就导致随机划分的结果偏乐观。我自己用随机划分能跑到97%以上的OA但换成按块划分的严格评估大概会掉到92%到94%。做入门实验没问题但如果要写论文、和别人的方法公平对比必须在论文里写清楚你的划分方式。5.2 类别不均衡Oats只有20个样本损失函数必须加权还记得那个只有20个样本的Oats类吗如果直接用普通交叉熵网络会把所有Oats样本当成噪声因为预测错这20个样本对总loss的贡献微乎其微。解决办法是给损失函数加类别权重样本越少的类权重越大counts np.bincount(y_train) weights len(y_train) / (len(counts) * counts) class_weights torch.FloatTensor(weights) criterion nn.CrossEntropyLoss(weightclass_weights)这个公式的意思是一个类的权重和它的样本数成反比。Soybean-mintill有1700多个训练样本权重不到0.5Oats只有十几个样本权重会被拉到10以上。这样网络每把Oats分错一次代价都足够大它才会认真对待这些小类别。我对比过不加权重和加权重的结果加权之后Oats的召回率能从0%涨到60%以上而整体OA基本不掉。5.3 完整训练循环和最佳模型保存训练部分没有太多花样核心是训练时开train模式验证和测试时开eval模式并且每个epoch都记录验证集指标只在验证集表现更好的时候保存模型。from torch.utils.data import TensorDataset, DataLoader device torch.device(cuda if torch.cuda.is_available() else cpu) train_ds TensorDataset(torch.FloatTensor(X_train), torch.LongTensor(y_train)) val_ds TensorDataset(torch.FloatTensor(X_val), torch.LongTensor(y_val)) train_loader DataLoader(train_ds, batch_size128, shuffleTrue, pin_memoryTrue) model HSICNN().to(device) optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50) criterion criterion.to(device) best_oa 0.0 for epoch in range(50): model.train() for xb, yb in train_loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() loss criterion(model(xb), yb) loss.backward() optimizer.step() scheduler.step() model.eval() with torch.no_grad(): val_pred model(torch.FloatTensor(X_val).to(device)).argmax(1) val_oa (val_pred.cpu().numpy() y_val).mean() if val_oa best_oa: best_oa val_oa torch.save(model.state_dict(), best_hsicnn.pt) print(fepoch {epoch1:02d} | loss {loss.item():.4f} | val_oa {val_oa:.4f})optimizer用Adam初始学习率1e-3加一点weight_decay正则scheduler用CosineAnnealing让学习率在50个epoch里从1e-3平滑下降到接近0。这个组合在Indian Pines上很稳一般30个epoch以内就能收敛到95%以上的验证OA。用torch.no_grad()包裹验证逻辑至关重要否则验证阶段也会计算梯度显存和CPU内存都会白白消耗。5.4 随机种子让别人能复现你结果的底线我见过朋友跑同一个代码两次结果差两个百分点最后发现是没固定随机种子。Indian Pines样本量小随机性影响比大数据集明显得多。在脚本最开头加上import random import numpy as np import torch random.seed(42) np.random.seed(42) torch.manual_seed(42) if torch.cuda.is_available(): torch.cuda.manual_seed_all(42) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark Falsecudnn.benchmark False会牺牲一点训练速度换取卷积算法的确定性。如果不关心精确复现可以只设前三个随机种子速度更快。6. 评估与成图OA、AA、Kappa一个都不能少6.1 三个指标各自盯着什么遥感分类论文里最常见的是三个指标OAOverall Accuracy、AAAverage Accuracy和Kappa系数。只看准确率会吃大亏我用一个具体例子说明如果模型把所有小于50样本的类别全部预测错也就是Alfalfa、Grass-pasture-mowed、Oats、Stone-Steel-Towers四类全错这四类加起来只占全部样本的1.8%OA只掉2个百分点左右看起来还是很高。但AA是每个类别先算自己的精度再取平均这四类全是016类平均下来AA会掉7个百分点以上。Kappa系数衡量的是预测和真实标签的一致性同样会明显下降。所以在报告结果时三个指标必须一起给否则很容易被表面上的高准确率欺骗。排序是OA反映整体表现AA反映小类表现Kappa反映一致性。6.2 计算指标、混淆矩阵与每类精度用scikit-learn几行就能算全from sklearn.metrics import accuracy_score, cohen_kappa_score, confusion_matrix model.load_state_dict(torch.load(best_hsicnn.pt)) model.eval() with torch.no_grad(): test_logits model(torch.FloatTensor(X_test).to(device)) y_pred test_logits.argmax(1).cpu().numpy() oa accuracy_score(y_test, y_pred) cm confusion_matrix(y_test, y_pred) aa (cm.diagonal() / cm.sum(axis1)).mean() kappa cohen_kappa_score(y_test, y_pred) print(fOA: {oa:.4f} | AA: {aa:.4f} | Kappa: {kappa:.4f})在这个划分下我通常得到的参考范围是OA 96%到98%AA 94%到97%Kappa 0.95到0.97。每次换个随机种子结果会在这个区间里波动这是正常的。如果你想看每个类别的表现把cm.diagonal() / cm.sum(axis1)打印出来重点关注Oats、Grass-pasture-mowed这些样本极少的类别它们的单类精度最能暴露模型的问题。6.3 生成整幅145×145分类图论文里的彩色分类图是怎么来的方法是把整幅145×145图像的每个像素都当成中心裁出patch送进网络预测再把预测结果拼回二维矩阵。这一步如果用普通循环写145×14521025个patch在CPU上要跑很久高效做法是用sliding_window_view一次性取出所有patchfrom numpy.lib.stride_tricks import sliding_window_view def build_full_pred_map(model, img_pca, patch_size11, batch_size512): half patch_size // 2 H, W, C img_pca.shape padded np.pad(img_pca, ((half, half), (half, half), (0, 0)), modereflect) windows sliding_window_view(padded, (patch_size, patch_size), axis(0, 1)) patches windows.reshape(-1, patch_size, patch_size, C) device next(model.parameters()).device preds [] model.eval() with torch.no_grad(): for i in range(0, len(patches), batch_size): batch torch.FloatTensor(patches[i:i batch_size]).permute(0, 3, 1, 2).to(device) pred model(batch).argmax(1).cpu().numpy() preds.append(pred) return np.concatenate(preds).reshape(H, W) 1 pred_map build_full_pred_map(model, img_pca)这里sliding_window_view在填充后的数组上滑动窗口得到的windows形状是(145, 145, 11, 11, 30)正好对应每个像素位置的patch所以reshape后顺序完全不会乱。最后加1是因为网络输出是0到15要映射回原始标签的1到16。画图时遮挡掉无标注区域import matplotlib.pyplot as plt from numpy.ma import masked_array fig, axes plt.subplots(1, 2, figsize(12, 5)) axes[0].imshow(gt, cmaptab20) axes[0].set_title(Ground Truth) axes[0].axis(off) masked_pred masked_array(pred_map, mask(gt 0)) axes[1].imshow(masked_pred, cmaptab20) axes[1].set_title(Prediction) axes[1].axis(off) plt.tight_layout() plt.savefig(classification_map.png, dpi200)跑完你会看到预测图的大块地物边界和真实标签基本一致但一些细碎区域尤其是不同作物混种的边缘会出现椒盐状的点状噪声。这是逐像素patch分类的典型现象想要更平滑的图后面可以接条件随机场或马尔可夫随机场做后处理那就是另一个话题了。7. 实战踩坑Indian Pines的常见翻车点7.1 读mat文件撞上MATLAB v7.3scipy.io.loadmat对老版本MATLAB的.mat文件支持得很好Indian Pines的标准文件是旧格式正常能读。但如果你从某些渠道下载到的文件是MATLAB v7.3格式scipy会直接报NotImplementedError提示要用HDF reader。解决办法是改用h5py读取但要注意h5py读出来的数据要转置维度顺序和scipy完全相反。我的建议是优先找标准格式的Indian_pines_corrected.mat少给自己添麻烦。7.2 标签从1开始忘记减1的连锁反应地面真值矩阵里像素值1到16对应16类0是背景。创建训练样本时要记得gt[r, c] - 1把标签变成0到15。漏掉这一步的后果有两种如果CrossEntropyLoss的类别数设为16模型会报索引越界错误如果你把类别数设为17模型能训练但最后评估时所有标签和预测都对不上整个结果作废。这个错误通常不会在训练时报错而是在出图或算指标时露出马脚非常隐蔽。7.3 全图推理时显存或内存爆掉训练时batch_size128没有问题但全图推理时如果你一次性把所有21025个patch都转成Tensor丢进GPU显存会瞬间飙高。我的做法是像build_full_pred_map里那样按512个patch一批处理预测完就释放。CPU推理同理分批能显著降低内存占用。另外torch.FloatTensor(patches[i:i batch_size])每次会新建一个临时数组如果你的原始数据是float64记得在PCA之后统一转成float32否则内存会翻倍。7.4 结果好到反常先检查数据泄漏如果你的OA跑到99.5%以上而且怎么看都不合理先别高兴大概率是泄漏了。最常见的两个来源一是PCA在全部数据上fit前面已经说过二是随机划分导致大面积同类地块的patch同时出现在训练集和测试集。我见过有人拿随机划分的结果去对比别人用块状划分的结果声称自己涨了好几个点这其实没有可比性。严格实验中应该先在训练集上fit PCA再transform测试集并且考虑按空间块划分数据来评估泛化能力。7.5 损失降了但验证指标来回跳Indian Pines样本量小加了类别权重之后训练loss的曲线会比普通任务更抖。验证OA在某个epoch涨到97%下一个epoch掉到95%再下一个又涨回来这是正常的。应对办法就是前面代码里的“只在验证集提升时保存模型”最后用保存的best_hsicnn.pt做测试而不是用最后一个epoch的权重。我的习惯是设置早停patience为15个epoch连续15个epoch验证指标不涨就停止训练省时间也避免过拟合。最后给一个我自己的习惯每次改结构、改超参数都固定一组随机种子跑三遍并取平均不要被单次结果骗过去。Indian Pines是个小数据集单次实验的波动比想象中大得多跑三遍取平均之后你才能真正看清一个改动是变好了还是单纯是运气。