简介:面向遥感影像地物分类与深度学习入门者,这份基于PyTorch的CNN分类资源提供了从Landsat数据预处理到模型训练与预测的完整Python实现。代码包含训练脚本、预测脚本与样本生成工具,配套示例影像和预训练模型,可直接运行并体验从影像切割到分类出图的完整流程。资源共10个文件,其中3个py脚本负责建块、训练与预测,2个tif为原始与分类示例影像,1个h5为训练好的权重,另有xml/tfw描述坐标系信息及markdown说明文档,包体约14.88MB。已有88人学习,适合高校学生用于课程设计、毕业设计或作为入门深度学习的实战项目;代码结构清晰,注释完整,可帮助理解CNN在遥感图像分类中的应用思路,也便于在此基础上针对不同地物类别进行迁移改进。
1. CNN 深度学习遥感影像地物分类:这份 Landsat Python 源码把切片、训练、预测整条链路串成了闭环
做遥感影像地物分类这件事,十有八九卡在同一个地方:好不容易拿到一块 Landsat 影像,想用 CNN 深度学习模型做地物分类,却不知道从哪一步开始下手。网上教程要么只讲模型结构,不碰数据预处理;要么给你一段训练代码,却没有完整的预测链路。这份 Python 源码包恰好反着来——1_createImageChips.py 把大影像切成训练样本,2_trainModel.py 训练七分类 CNN 模型,3_predictNewData.py 拿着训练好的权重预测新影像,配合 CNN_7class_3by3.h5 预训练权重,把「数据准备 → 模型训练 → 推理预测」整条链路一次性铺开。它适合毕设、课设,也适合刚接触遥感深度学习的初学者跟着跑通一遍,弄清楚每个环节在做什么。压缩包里的说明.md 把每个文件的用途和运行顺序写得很清楚,按编号走流程基本不会迷路。
2. Landsat 影像切片:1_createImageChips.py 是怎么把大影像切成 CNN 能吃的样本
2.1 为什么必须切块:Landsat 影像尺寸和 CNN 固定输入的矛盾
Landsat 8 OLI 的单景影像大约是 7700×7800 像素,如果选 7 个波段参与分类,读进内存就是一个 (7, 7700, 7800) 的数组。这个尺寸直接送进 CNN 是不可能的——卷积层勉强能滑动,全连接层会把参数数量撑到爆炸。而且影像内部不同区域地物差异大,一整幅影像笼统地训练一个全局模型,等于强迫模型对所有地物一视同仁,精度必然打折。
切片(Image Chips)的本质是把分类问题从「一整幅影像是什么」拆成「每个小块是什么」,每个块独立作为一个样本参与训练。块的尺寸决定了模型能看到多少上下文信息。这份源码里窗口取 64×64,对应 30 米分辨率的 Landsat 数据,一个样本覆盖大约 1.9×1.9 平方公里的范围,对水体、农田、建筑区这类地物,64 窗口足够容纳纹理特征和空间上下文。如果你用的是 2 米以下分辨率的航拍影像,窗口就得放大到 128 或 256,否则一个窗口里可能只有半栋楼,模型什么都学不到。
窗口大小和步长之间还有联动关系。滑动窗口的步长如果等于窗口大小,切出来的样本没有重叠,样本量最小;步长小于窗口,相邻样本之间就有重叠区域,等于做了隐式的数据增强。这份源码选 64 窗口配 32 步长,重叠率 50%,是遥感切片里非常常见的组合,兼顾了样本量和空间连续性。
2.2 切片脚本拆解:窗口、步长、标签对齐是怎么实现的
1_createImageChips.py 的核心逻辑我按可复现的版本还原一下。它全程依赖 GDAL 读栅格,所以环境里必须有 gdal 库,建议用 conda 装,一句conda install gdal就能把底层依赖带全;pip 直接装经常因为 numpy 版本不匹配而编译失败。安装时顺便确认 numpy 可用,切片的数组操作全部建立在 numpy 上。
import numpy as np from osgeo import gdal # 打开多波段影像和标注栅格,实际路径按你的数据改 ds_img = gdal.Open('example.tif') img = ds_img.ReadAsArray() # shape: (波段数, 行数, 列数) ds_label = gdal.Open('label.tif') label = ds_label.ReadAsArray() # shape: (行数, 列数) # 影像和标注可能差一行一列,先统一到公共范围 rows = min(img.shape[1], label.shape[0]) cols = min(img.shape[2], label.shape[1]) img = img[:, :rows, :cols] label = label[:rows, :cols] window_size = 64 # 切片窗口边长 stride = 32 # 滑动步长,小于窗口产生重叠 samples, targets = [], [] for row in range(0, rows - window_size + 1, stride): for col in range(0, cols - window_size + 1, stride): chip = img[:, row:row + window_size, col:col + window_size] label_chip = label[row:row + window_size, col:col + window_size] # 统计标签块里每类的像素数,主类别占比过低就丢弃 classes, counts = np.unique(label_chip, return_counts=True) dominant_idx = np.argmax(counts) dominant_class = classes[dominant_idx] # 主类别占比低于 80% 的样本大多落在地物边界上,标签不可靠 if counts[dominant_idx] / (window_size * window_size) < 0.8: continue samples.append(chip) targets.append(dominant_class) # 转成 NHWC 格式:样本数、高、宽、波段数 samples = np.array(samples).transpose(0, 2, 3, 1) targets = np.array(targets) np.save('chips.npy', samples) np.save('labels.npy', targets) print('samples:', samples.shape, 'targets:', targets.shape)这段脚本里三个细节值得单独说。第一个是 ReadAsArray 读出来的顺序,多波段影像读出来是 (波段数, 行, 列) 的 CHW 布局,而 TensorFlow 卷积层默认要 NHWC,所以最后必须做 transpose(0, 2, 3, 1),顺序错了后面训练直接报维度不匹配。第二个是标签的处理方式,注意这里不是逐像素分类,而是给整个窗口一个整数标签——取窗口内像素占比最高的类别。第三个是主类别占比的过滤条件,0.8 这个阈值的意思是窗口里必须有 80% 以上的像素属于同一类才保留,否则说明窗口正好压在地物边界上,标签本身就不干净,喂给模型只会放大噪声。
参数上,window_size 决定上下文窗口的大小,stride 决定样本之间的重叠率。如果样本量太少,先把 stride 调小让重叠率上去;如果样本量够、想跑快一点,stride 可以直接等于 window_size。有一点值得注意,80% 这个过滤条件在边界特别碎的数据上会丢掉大量样本,我一般会降到 0.6 再试一次,对比两版样本量再决定。
2.3 切片之后先检查:样本形状、类别分布和标准化参数
切片脚本跑完,第一件事不是急着训练,而是先确认输出对不对,顺手把训练集的标准化参数存下来。用几行代码就能把这几个信息一次拿到:
import numpy as np chips = np.load('chips.npy') labels = np.load('labels.npy') print('chips shape:', chips.shape) # (样本数, 64, 64, 7) print('labels shape:', labels.shape) # (样本数,) print('class counts:', np.bincount(labels)) # 顺便存训练集的均值和标准差,预测阶段还要用 train_mean = chips.reshape(-1, chips.shape[-1]).mean(axis=0) train_std = chips.reshape(-1, chips.shape[-1]).std(axis=0) np.save('train_mean.npy', train_mean) np.save('train_std.npy', train_std)chips.shape 里的最后一位必须是波段数,这份源码里是 7,对应 Landsat 的 7 个波段。如果这个数字变成 1 或者和你预期不符,多半是 GDAL 读出来的波段数不对,或者影像本身是单波段数据。class counts 输出每一类样本的个数,比如 [5230, 1120, 860, 4100, 2300, 980, 1500],这里能直接看到哪几类样本偏少,后面训练时好对症下药地做类别加权。
均值和标准差这两个 .npy 文件务必保存,预测脚本要用。标准化是训练和预测共用的一套统计量,训练时用自己的均值,预测时也用自己的均值,两边不一致模型直接废掉。波段顺序也值得确认一次:Landsat 8 是 B2 蓝、B3 绿、B4 红、B5 近红外、B6/B7 短波红外;Landsat 5/7 的 TM/ETM 则是 B1 蓝、B2 绿、B3 红、B4 近红外。切片脚本读进来的是文件里的波段顺序,不保证是你理解的那个顺序,跑 RGB 合成时下标对应错了会出现红绿通道颠倒。
3. 训练七分类模型:2_trainModel.py 的模型结构、损失函数与参数调整
3.1 从文件名读模型:7class、3by3 背后的结构选择
看到 CNN_7class_3by3.h5 这个名字,先别急着跑代码,把信息读出来再动手。7class 指最终输出是 7 类地物,3by3 指卷积核尺寸是 3×3。这两个信息基本就把模型骨架定死了:输入一个 64×64×7 的影像块,经过若干层 3×3 卷积和池化提取特征,展平后接全连接层,最后 softmax 输出 7 类概率。
在遥感影像分类里,3×3 卷积核是默认选择,原因很实际:两个堆叠的 3×3 卷积感受野等价于一个 5×5 卷积,但参数量只有后者的十八分之五左右,非线性还更强。网络层数不需要太深,七分类地物识别靠的是光谱值和局部纹理差异,浅层网络在几万样本量下比深网容易收敛、不容易过拟合。模型定义用 TensorFlow Keras 还原出来是这个样子:
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout from tensorflow.keras.optimizers import Adam model = Sequential([ Conv2D(32, (3, 3), activation='relu', padding='same', input_shape=(64, 64, 7)), MaxPooling2D((2, 2)), Conv2D(64, (3, 3), activation='relu', padding='same'), MaxPooling2D((2, 2)), Flatten(), Dense(128, activation='relu'), Dropout(0.5), Dense(7, activation='softmax') ]) model.compile( optimizer=Adam(learning_rate=0.001), loss='sparse_categorical_crossentropy', metrics=['accuracy'] )第一层 Conv2D 的 input_shape 是 (64, 64, 7),三个数字必须和切片脚本里的窗口大小、波段数严格一致,改了一个另一个不跟着改,训练时直接报维度错误。padding='same' 保证卷积不改变特征图尺寸,所以每次 MaxPooling 把宽高减半。Dropout(0.5) 放在全连接层之前,训练时随机丢弃一半神经元,是几万样本量下最便宜的防过拟合手段。学习率 0.001 是 Adam 的默认值,在遥感数据上基本不用动,真遇到 loss 震荡优先降到 0.0005 试试。
3.2 训练脚本的关键参数:早停、模型保存和类别权重
模型定义好之后,fit 才是真正花时间的地方。这里有几个参数值得认真对待,它们直接决定你最后拿到的是能用的模型还是只能看 loss 曲线的半成品:
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint history = model.fit( x_train, y_train, validation_split=0.2, epochs=50, batch_size=32, class_weight={ 0: 1.0, 1: 3.5, # 样本少的类别加权 2: 1.0, 3: 5.0, 4: 1.2, 5: 1.0, 6: 2.8 }, callbacks=[ EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True), ModelCheckpoint('CNN_7class_3by3.h5', monitor='val_loss', save_best_only=True) ] )epochs=50 配 EarlyStopping 的 patience=5,意思是模型最多跑 50 轮,但验证集 loss 连续 5 轮不下降就提前停,并自动恢复到验证集表现最好的权重。遥感数据有个特点:训练集和验证集往往来自同一幅影像的相邻区域,空间自相关很强,训练后期验证 loss 容易先降后升,早停能把模型拦在过拟合之前。ModelCheckpoint 这里只保存验证 loss 最低那一轮的权重,保证磁盘上的 .h5 永远是最优版本而不是最后一轮的结果。
batch_size=32 在 64×64×7 的输入下大约吃 2-3 GB 显存,GTX 1060 级别就能跑。显存不够的机器,先把 batch_size 减半,不要一上来就改网络结构。class_weight 是给损失函数里每个类别乘一个系数,样本少的类别系数大,模型会更关注这些样本。权重设置的原则是样本占比越低权重越高,但单个权重最好不要超过 10,否则模型会为了讨好稀缺类而牺牲常见类的精度。
3.3 训练完之后别直接收工:混淆矩阵和分类报告能告诉你什么
训练结束后,光看 accuracy 不够,七分类任务里一个类别准确率 95% 而另一个只有 30% 的情况非常常见,要把混淆矩阵打出来看类别之间的具体误判关系:
from sklearn.metrics import classification_report, confusion_matrix import numpy as np y_pred = np.argmax(model.predict(x_valid), axis=-1) print(confusion_matrix(y_valid, y_pred)) print(classification_report(y_valid, y_pred))混淆矩阵每一行是真实类别,每一列是预测类别,对角线上的数越大越好。我一般先看对角线之外最大的那个数在哪里,那基本就是模型最容易混淆的两个类别,比如建筑和裸土、水体和阴影。知道模型在哪两类之间犯糊涂,后续的改进方向就明确了——要么加波段,要么给这两类加训练样本。classification_report 里的 f1-score 对类别不均衡比 accuracy 可靠得多,尤其适合七分类这种存在明显样本量差异的任务。
如果你更习惯 PyTorch,这套模型的迁移也不难:Conv2D 换成 torch.nn.Conv2d,MaxPooling2D 换成 torch.nn.MaxPool2d,训练循环自己写,数据加载用 DataLoader。但 CNN_7class_3by3.h5 是 Keras 的权重格式,PyTorch 直接加载不了,要么用脚本转换权重成 state_dict,要么先用原始 Keras 环境把整个流程跑通。我的建议是第一次复现先不要换框架,h5 文件把网络结构定义和权重打包在一起,load_model 一行就能全部恢复,省去手写结构对权重的麻烦。
4. 预测新影像:3_predictNewData.py 从 example.tif 到 new_class.tif 的完整推理链路
4.1 推理链路拆解:加载模型、标准化、滑动预测
训练拿到模型之后,真正的考验是用它预测一幅全新的影像。3_predictNewData.py 做的就是这件事:输入 example.tif,输出 new_class.tif。整个推理链路分成四步,缺一步预测结果就不对:
import numpy as np from osgeo import gdal from tensorflow.keras.models import load_model # 第一步:加载训练好的模型 model = load_model('CNN_7class_3by3.h5') # 第二步:读取待预测影像 ds = gdal.Open('example.tif') img = ds.ReadAsArray() rows, cols = img.shape[1], img.shape[2] # 第三步:标准化——必须用训练集统计量,不能自己现算 train_mean = np.load('train_mean.npy') train_std = np.load('train_std.npy') img = (img.astype(np.float32) - train_mean[:, None, None]) / (train_std[:, None, None] + 1e-6) # 第四步:滑动窗口预测,填回结果数组 window = 64 result = np.zeros((rows, cols), dtype=np.uint8) for r in range(0, rows - window + 1, window): for c in range(0, cols - window + 1, window): chip = img[:, r:r + window, c:c + window] chip = np.transpose(chip, (1, 2, 0))[None, ...] pred = model.predict(chip, verbose=0) result[r:r + window, c:c + window] = np.argmax(pred[0], axis=-1)第三步是最容易翻车的地方。训练时样本做了减均值除以标准差的标准化,预测时如果用待预测影像自己的均值和标准差,输入数据分布就变了,模型精度会肉眼可见地往下掉。正确的做法就在代码里:切片阶段把训练集的 mean 和 std 存成 .npy 文件,预测阶段原封不动读回来。我自己做项目会把标准化这一步提成公共函数,训练和预测共用一个函数,从根上杜绝两套标准不一致。
第四步的窗口参数 window=64 必须和训练时完全一致。模型学的是 64×64 输入到类别的映射,预测时切成别的尺寸,模型结构都对不上,直接报维度错误。预测遍历的步长取了 window,也就是没有重叠,原因很简单:预测阶段不需要数据增强,每个像素只要被预测一次就够了。
4.2 new_class.tif 之外的那几个文件是干什么的
预测脚本跑完,目录里会多出 new_class.tif、new_class.tfw、new_class.tif.aux.xml、new_class.tif.xml 这几个文件。它们不是垃圾文件,各自有各自的作用:
| 文件 | 作用 | 能不能删 |
|---|---|---|
| new_class.tif | 分类结果栅格,灰度值即类别编号 | 不能删 |
| new_class.tfw | 世界文件,记录像元尺寸、旋转参数、左上角坐标 | 建议保留 |
| new_class.tif.aux.xml | GDAL 生成的栅格统计缓存,记录直方图等 | 可以删 |
| new_class.tif.xml | 元数据描述文件 | 可以删 |
new_class.tfw 是地理参考信息的关键。很多从深度学习流程里出来的 tif 只有图像数据,没有地图定位信息,用 ArcGIS 打开之后要么跑到原点要么根本显示不出来。tfw 文件记录的就是像元在地图坐标系里的位置、尺寸、旋转方向。这份源码里 new_class.tif 能正常叠加到原始影像上,靠的就是生成时把仿射变换参数写进了 tfw。
.aux.xml 是 GDAL 顺手生成的缓存,记录栅格统计信息和色彩解释。删掉不影响分类结果,只是下次打开软件时重新计算一遍统计值,影像大了会感觉慢一点。如果你要把结果分享给别人,保留 tif 和 tfw 就够了,两个 xml 文件可以清掉。
4.3 边缘处理与坐标对齐:预测脚本里最容易翻车的两处细节
预测结果写栅格之前,完整写入逻辑值得单独贴一下,这里有两个细节处理不好,输出图就是错的:
from osgeo import gdal driver = gdal.GetDriverByName('GTiff') out_ds = driver.Create('new_class.tif', cols, rows, 1, gdal.GDT_Byte) # 复制原始影像的地理参考信息 out_ds.SetGeoTransform(ds.GetGeoTransform()) # 仿射六参数 out_ds.SetProjection(ds.GetProjection()) # 坐标系 WKT out_band = out_ds.GetRasterBand(1) out_band.WriteArray(result) out_band.FlushCache() out_ds = None第一个细节是 SetGeoTransform 和 SetProjection 两个调用缺一不可。SetGeoTransform 管位置,SetProjection 管坐标系,少一个分类图在 GIS 里要么位置飘了,要么单位和坐标系对不上。第二个细节是边缘地带。滑动循环里用了 rows - window + 1,意味着影像右边界和下边界会剩一排像素处理不到,这部分保持 result 数组初始化的 0 值。处理办法有几种,最简单的是给影像先 pad 一圈再预测,预测完裁掉 pad 区域;复杂一点的可以最后一排单独用变尺寸切片处理。大部分项目里边缘剩余一两个像素不影响整体评估,但如果研究区裁剪得碎,这个现象会被放大,值得提前考虑。
坐标对齐还有一个隐蔽的坑:如果切片阶段对影像做过裁剪或重投影,预测阶段的 gdal.Open 必须在同一个文件路径上操作,不能把 example.tif 搬走或者替换成别的影像。tfw 文件里的仿射变换参数和影像的像素行列是一一对应的,影像换了,参数就失效了。
5. 常见问题排查:跑这份 Landsat 分类源码时我踩过的五个坑
5.1 现象:切片脚本报 IndexError,数组形状对不上
切片脚本第一轮循环就报 index 3 is out of bounds for axis 0 with size 2,或者类似的越界错误。原因基本是影像和标注栅格的行列数不一致。Landsat 产品经过不同方式的裁剪、镶嵌后,边缘经常差一行或一列。GDAL 只是按文件头信息把数据读成数组,不会自动帮你对齐。
解决顺序有讲究:先在脚本开头打印 img.shape 和 label.shape,确认差多少,然后统一裁到公共范围,再做后续任何操作。我自己的项目里会把这一步提成函数,每次读栅格都过一遍,避免在切片循环里才发现问题。注意裁剪的时候影像和标注要就小不就大,取两者行列数的 min,否则超出边界的部分会被填成无效值,样本质量反而更差。
5.2 现象:load_model 加载 h5 报错,错误信息看不懂
3_predictNewData.py 里 model = load_model('CNN_7class_3by3.h5') 直接抛异常,常见的有 AttributeError: 'str' object has no attribute 'decode',或者 Unknown layer。原因大部分是 TensorFlow 和 Keras 的版本不匹配。h5 文件把网络结构定义和权重打包在一起,加载时按结构定义里的层类路径反序列化,版本一换,类的位置变了,自然加载失败。
解决的思路是固定版本而不是升级。TensorFlow 2.10 之前的版本用 tf.keras 接口加载,旧版本生成的权重用新版加载偶尔会碰到兼容性报错。真遇到这种情况,新建虚拟环境装回和这份源码生成时一致的大版本,load_model 基本都能过。不要在报错的机器上来回升降级 TensorFlow,那会把环境搞成一团乱麻。
5.3 现象:预测结果整体错位,叠加到原图上边界对不齐
new_class.tif 在 ArcGIS 里叠加到原始影像上,地物边界整体偏移一格或者半格,看起来像分类图往右下角平移了。原因不是地理参考写错了,而是预测循环和切片循环的窗口起点不一致。比如切片从第 0 行第 0 列开始,预测却从 stride 开始,或者 predict 循环里步长跟训练切片不一致,都会造成窗口对齐偏移。
排查方法是把训练切片脚本和预测脚本的循环条件并排放在一起,逐行比对 range 的起点、终点、步长三要素。两套循环的窗口参数、padding 策略、标准化方式都必须完全一致。我一般会在两个脚本里都加上 window 和 stride 的打印,跑之前肉眼确认一遍,防止改了一个忘了另一个。
5.4 现象:某一类地物精度特别低,和另一类混在一起
看 confusion_matrix,水体和阴影互相误判、建筑和裸土混在一起,这类问题在七分类里太常见了。原因有两个方向:一是这两类本身光谱特征接近,Landsat 的 30 米分辨率下混合像元本来就难分;二是训练样本里这两类的数量不均衡,模型为了整体 loss 放弃了样本少的那一类。
解决分两步。第一步给稀有类加 class_weight,让模型训练时更关注这些样本。第二步如果加完权重精度还上不去,考虑加波段特征——短波红外对水体和阴影的区分效果比可见光明显,很多地物分类模型输入里 SWIR 波段的贡献都很大。这份源码用的是 7 波段全套输入,如果混淆的还是那几个类位,可以试试只保留相关性低的波段核心组合,有时候降维比加维更有效。
5.5 现象:训练时 GPU 显存溢出,报 Resource exhausted
训练中途 OOM,报 Reserve has been exceeded 或者 Resource exhausted: OOM。原因就两类:输入尺寸太大导致中间特征图占显存过多,或者 batch_size 设置过大。64×64 窗口本身占不了多少显存,但如果 batch_size 拉到 128 以上、或者窗口改成 256 而没有同步减小 batch,显存占用就会指数上涨。
解法按优先级排列:先把 batch_size 减半到 16,不行再看 nvidia-smi 确认有没有其他进程占显存,再不行减小第一层卷积核数量从 32 降到 16。不要一上来就换网络结构,遥感小卷积网络对显存的需求没有想象中那么恐怖。预测阶段如果也爆显存,把预测循环改成按行切块,不要一次把整幅影像加载进模型。
6. 预测结果别急着交差:用 matplotlib 把分类图和原图并排叠起来
训练完成、new_class.tif 生成之后,很多人第一反应是打开 ArcGIS 加载影像,手动配符号系统。我有一个更快的验证习惯:直接用 matplotlib 把原始影像和分类结果并排打印到一张图里,几秒钟就能看出明显错分:
import numpy as np import matplotlib.pyplot as plt from osgeo import gdal ds = gdal.Open('example.tif') img = ds.ReadAsArray() # Landsat 8 真彩色合成:B4、B3、B2 对应下标 3、2、1 rgb = np.stack([img[3], img[2], img[1]], axis=-1) rgb = rgb / (np.percentile(rgb, 98) + 1e-6) ds_cls = gdal.Open('new_class.tif') cls = ds_cls.ReadAsArray() fig, axes = plt.subplots(1, 2, figsize=(14, 7)) axes[0].imshow(rgb) axes[0].set_title('Original RGB (B4-B3-B2)') im = axes[1].imshow(cls, cmap='tab20', vmin=0, vmax=6) axes[1].set_title('Predicted Classes') plt.colorbar(im, ax=axes[1]) plt.savefig('pred_check.png', dpi=200, bbox_inches='tight')rgb = np.stack([img[3], img[2], img[1]], axis=-1) 是从 7 个波段里挑出红绿蓝三个波段合成真彩色,拉伸用的是 98 分位而不是最大最小值,避免个别亮像素把整张图压暗。分类图用 tab20 色带染色,每类颜色区分度大,扫一眼就能发现哪些区域颜色明显不对。
验证时我会重点盯三个位置:水域和阴影的交界、农田和裸土的交界、城镇建筑密集区。这三处是七分类模型最容易翻车的地方。如果发现某两类总是混在一起,先别急着调网络,切出一小块区域放大看原始影像,判断到底是样本量不足、波段区分度不够,还是标签本身就有问题。从那以后我每次训练完都不急着看 loss 曲线,而是强制自己先跑一遍这张对比图,再谈精度指标;这种抽查方法在项目交付前能拦住大部分低级错误。希望帮到你,也祝你用这份源码跑出第一张像样的地物分类图。
本文还有配套的精品资源,点击获取