简介:本资源是一套面向高校学生与遥感图像处理初学者的完整语义分割实践项目,聚焦遥感影像地物分类任务,提供Deeplab V3+与U-Net两种主流模型的Python实现方案,适用于毕业设计、课程设计及期末大作业等学术场景。压缩包共11个文件(8.67MB),含4个核心Python脚本(模型构建、训练、数据预处理与可视化)、1份详尽的Word手册(含环境配置、运行步骤与结果分析)、5张遥感样本图像及1张原始遥感图,代码全程中文注释,结构清晰、模块解耦,便于理解模型流程与调试修改。目前已有334人学习下载,项目经实际验证效果良好,获导师高度认可,评分98分,下载后仅需配置基础深度学习环境即可一键训练与推理,是入门遥感图像智能解译的高性价比实践范本。 上个月接了一个遥感影像城市建筑物提取的项目,需求很直接:输入一张高分辨率遥感图,算法自动把建筑轮廓圈出来。这种任务在深度学习里就是标准的语义分割问题,做像素级分类。我在技术选型时没纠结太久,直接锁定了UNet和DeepLab V3+两套方案。一方面,这两个模型在Python生态里的资料足够多,遇到问题好排查;另一方面,它们代表了语义分割领域两条不同的技术路线,拿来做对比实验,后面写项目报告也更有说服力。整条链路跑下来,从数据切片、标注格式转换、模型训练到精度评估,大概花了两周半。这篇文章就是这次实践的完整复盘,内容包括两个模型的原理差异、Python源码实现、训练参数选择,以及我实际调参过程中踩过的坑,给正在做遥感图像分割的同学一个参照。
1. 项目整体设计与思路拆解
1.1 遥感图像分割和普通图像分割的差异
遥感图像语义分割,本质上是给影像中的每一个像素分配语义类别标签,比如建筑物、道路、水体、植被、耕地等。这个任务跟自然图像分割(比如人像分割、街景分割)有非常大的区别,主要体现在三个方面。
第一,图像尺寸巨大。一张高分辨率遥感影像动不动就是5000×5000甚至上万像素,直接送进深度学习模型,显存根本扛不住。所以必须做切片处理,把大图切成小图再逐个推理。第二,类别分布极度不平衡。像建筑物、道路这类目标在整幅图里占的面积比例往往只有百分之几。模型如果全部预测成背景,准确率也能到95%以上,但实际上一栋建筑都没提取出来。第三,地物尺度差异大。同一张图里可能既有占地几千平米的大型厂房,也有几十平米的民房,如果模型感受野固定,很容易顾此失彼。这三个特点直接决定了下游的模型选型和数据处理策略。
1.2 为什么同时选UNet和DeepLab V3+
这两个模型背后是完全不同的技术路线。UNet的核心是编码器-解码器结构加上跳跃连接,编码器逐层下采样提取语义特征,解码器逐层上采样恢复空间分辨率,跳跃连接把编码器每一层的特征直接拼到解码器对应层上,弥补下采样丢失的空间细节。这种设计对边界细节特别友好,而且在小样本数据集上表现稳定。
DeepLab V3+则走的是另一条路。它的核心是空洞卷积和ASPP模块(Atrous Spatial Pyramid Pooling,空洞空间金字塔池化),通过多个不同膨胀率的空洞卷积并行采样,相当于用一组不同分辨率的“眼镜”同时看图像,一个模块就能捕捉到多尺度的上下文信息。相比UNet,它在处理大尺度地物和复杂背景时上限更高。
我选这两个模型还有一个现实原因:一个轻量稳定,一个精度上限高,正好做对照组。实际操作中,UNet作为baseline快速打通流程,DeepLab V3+用来追求更高的精度。
1.3 项目环境与代码目录组织
项目主要使用Python 3.8作为运行环境,深度学习框架用TensorFlow 2.13(Keras接口)。选TF而不是PyTorch没有特别复杂的原因,主要是手头已有的预训练权重和数据处理代码都是TF生态的,迁移成本低。核心依赖如下:
numpy==1.24 opencv-python pillow tensorflow==2.13 albumentations rasterio scikit-learn代码目录结构我习惯用下面的方式组织,逻辑清晰,后面换数据集也方便:
remote_sensing_seg/ ├── data/ │ ├── raw/ # 原始遥感影像和标注 │ ├── train/ # 切片后的训练数据 │ └── val/ # 验证数据 ├── src/ │ ├── dataset.py # 数据读取与增强 │ ├── unet.py # UNet模型定义 │ ├── deeplabv3p.py # DeepLab V3+模型定义 │ ├── losses.py # 损失函数 │ ├── train.py # 训练入口 │ └── predict.py # 推理与后处理 ├── weights/ # 保存模型权重 ├── logs/ # TensorBoard日志 └── config.yaml # 配置参数项目实际用到的遥感数据集是公开的航空影像标注数据集,包含了城市区域的建筑物标注,共约3000张切片。数据集的标注以灰度掩码图形式提供,0表示背景,255表示建筑物。这种格式处理起来比较直接。
2. 遥感图像数据集准备与预处理
2.1 从原始影像到训练数据的关键步骤
遥感图像分割的整个数据准备流程,远没有很多人想的那么轻松。直接下载数据就开始训练是行不通的,中间至少有三个步骤必须做扎实:裁剪、格式转换、数据增强。
先说裁剪。原始遥感影像尺寸很大,我这边拿到的数据单张在2000×2000左右,如果不裁切,显存直接爆掉。常用的做法是把影像切成256×256或者512×512的小图。我这次选的是512×512,理由是感受野更大一些,模型能看到更多上下文。但要注意,切片的时候必须加重叠区域(overlap),我的做法是512的切片大小,步长(stride)取256,也就是每一张切片跟相邻切片有128像素的重叠。重叠的目的,是避免某个建筑物刚好被切在两张切片的边界上,导致标签被切断,模型学到残缺的目标。
另外,切片时要特别关注带标注的掩码图有没有错位。遥感影像和标注经常来自不同渠道,偶尔会有几个像素的偏移。如果直接训练,会引入大量噪声。
2.2 数据增强策略与边界把控
数据增强是遥感图像分割里决定成败的一环。我用的库是albumentations,这个库的好处是图像和掩码同步变换,不会出现只变了图、掩码没跟着变的问题。我最后采用的增强策略如下:
import albumentations as A train_transform = A.Compose([ A.HorizontalFlip(p=0.5), A.VerticalFlip(p=0.5), A.RandomRotate90(p=0.5), A.RandomBrightnessContrast(p=0.3, brightness_limit=0.1, contrast_limit=0.1), A.RandomCrop(height=512, width=512, p=1.0), ])这里面有几个容易踩的坑。第一,翻转操作对遥感图是安全的,因为遥感影像是俯视图,不存在上下左右的方向性问题;但对街景这种正视图像,垂直翻转就不合适了。第二,不要对遥感图做强烈的仿射变换,比如随机缩放、旋转任意角度。强烈仿射会让建筑物的几何形状严重变形,模型学到的特征跟真实场景对不上,测试时精度反而掉。第三,颜色抖动要克制。遥感影像的光谱信息是重要的判别特征,如果颜色增强太强,建筑物和道路这种本来颜色就接近的地物会更容易混淆。我实测下来,亮度和对比度小幅调整就够了,色相和饱和度基本不动。
2.3 Dataset读取与One-Hot编码
数据读取这块,我写了一个简单的TensorFlow Dataset对接层。核心逻辑是把图片和掩码文件路径读进来,分别解码、归一化、做One-Hot编码。
import tensorflow as tf def read_image(path): img = tf.io.read_file(path) img = tf.image.decode_jpeg(img, channels=3) img = tf.image.resize(img, (512, 512)) img = img / 127.5 - 1.0 return img def read_mask(path): mask = tf.io.read_file(path) mask = tf.image.decode_png(mask, channels=1) mask = tf.image.resize(mask, (512, 512), method='nearest') # 二值掩码转One-Hot mask_binary = tf.cast(mask > 0, tf.int32) one_hot = tf.one_hot(mask_binary, depth=2) return tf.squeeze(one_hot, axis=-1)这里有个细节:掩码在resize的时候,method必须指定为nearest。如果用默认的双线性插值,掩码里靠近边界的位置会生成中间灰度值,比如0.5,这个像素点既不是前景也不是背景,训练的时候模型的梯度是乱的。同样的道理,在预处理阶段把掩码转成One-Hot,也要保证类别索引是正确的整数。
3. UNet模型在Python中的实现与训练
3.1 UNet结构拆解:跳跃连接为什么有效
UNet这个名字的来源,就是它的结构看起来像一个字母U。左侧是编码器,右侧是解码器,中间底部是一个瓶颈层。编码器做卷机和下采样,每下采样一次,特征图尺寸减半、通道数加倍;解码器做上采样,每次先转置卷积或插值放大,再把编码器对应层的特征图拼过来。
跳跃连接是整个UNet的灵魂。没有跳跃连接,网络就是一条普通的编码器-解码器管道,下采样丢失的细节永远回不来。有了跳跃连接,解码器可以直接拿到编码器在高分辨率阶段提取的浅层特征,这些特征含有丰富的边缘和纹理信息,对恢复建筑物的精细轮廓至关重要。我在实际实验里对比过去掉跳跃连接的版本,mIoU直接掉了近6个百分点,可见这个设计有多重要。
3.2 可直接运行的UNet Keras实现
下面是我项目中实际使用的UNet简化版代码,去掉了不必要的复杂部分,保持了清晰的层次结构。
import tensorflow as tf from tensorflow.keras.layers import ( Input, Conv2D, MaxPooling2D, UpSampling2D, Concatenate, BatchNormalization, Dropout, Conv2DTranspose ) from tensorflow.keras.models import Model def conv_block(x, filters, kernel_size=3, dropout_rate=0.1): x = Conv2D(filters, kernel_size, padding='same')(x) x = BatchNormalization()(x) x = tf.keras.layers.ReLU()(x) x = Dropout(dropout_rate)(x) x = Conv2D(filters, kernel_size, padding='same')(x) x = BatchNormalization()(x) x = tf.keras.layers.ReLU()(x) return x def encoder_block(x, filters): skip = conv_block(x, filters) pool = MaxPooling2D(pool_size=(2, 2))(skip) return skip, pool def decoder_block(x, skip, filters): x = Conv2DTranspose(filters, kernel_size=(2, 2), strides=2, padding='same')(x) x = Concatenate()([x, skip]) x = conv_block(x, filters) return x def build_unet(input_shape=(512, 512, 3), num_classes=2): inputs = Input(shape=input_shape) # Encoder s1, p1 = encoder_block(inputs, 64) s2, p2 = encoder_block(p1, 128) s3, p3 = encoder_block(p2, 256) s4, p4 = encoder_block(p3, 512) # Bottleneck bridge = conv_block(p4, 1024) # Decoder d1 = decoder_block(bridge, s4, 512) d2 = decoder_block(d1, s3, 256) d3 = decoder_block(d2, s2, 128) d4 = decoder_block(d3, s1, 64) outputs = Conv2D(num_classes, 1, activation='softmax')(d4) model = Model(inputs, outputs) return model3.3 UNet训练的核心参数与BatchSize陷阱
UNet的训练参数我最终定为:输入尺寸512×512,batch size 8,初始学习率1e-3,优化器Adam,训练100轮。
这里有一个非常隐蔽的坑,我一开始没注意到。UNet网络本身不算很深,但batch size只有8时,BatchNormalization层的统计量会很不稳定。BN层在训练时用的是当前batch内的均值和方差,batch太小,这两个统计量波动很大,导致验证集上的精度跟着震荡,时好时坏。
应对办法有两种。一是把batch size调大,但这会受显存限制;二是换用GroupNormalization或InstanceNormalization,它们不依赖batch内的统计量,在batch size小的场景下更稳定。我实测发现,batch size为8时,UNet用BN和用GN的最终mIoU差了将近2个百分点。所以如果你也在用比较小的batch size训练分割模型,遇到验证精度一直抖动,先考虑换归一化层。
4. DeepLab V3+模型在Python中的实现与训练
4.1 DeepLab V3+的网络结构与ASPP模块原理
DeepLab V3+在结构上可以分为编码器(Encoder)和解码器(Decoder)两段。编码器部分使用骨干网络提取特征,然后接一个ASPP模块。ASPP模块的含义是“空洞空间金字塔池化”,它用一组不同膨胀率的空洞卷积去并行处理同一份特征图。
空洞卷积的作用可以在不增加参数量的前提下扩大感受野。膨胀率(dilation rate)表示卷积核相邻点的间隔,rate=1就是普通卷积,rate=6表示卷积核间隔5个像素采样。膨胀率越大,感受野越大,能看到的上下文越广。ASPP同时使用rate=6、12、18的卷积,等于让模型分别从近、中、远三个尺度看目标,最后把结果拼起来,这样无论是小房子还是大工厂都能被照顾到。
解码器部分也是DeepLab V3+区别于早期DeepLab版本的关键。ASPP输出的特征图先上采样4倍,再跟骨干网络底层的低层特征拼接,用卷积融合。低层特征分辨率高、细节多,正好补齐空洞卷积特征图在多次下采样中丢失的边界信息。这个设计思路和UNet的跳跃连接如出一辙,但实现细节和复杂度不太一样。
4.2 ASPP模块代码与实现要点
以下是我实现的ASPP模块,基于Keras编写的,兼容TF 2.x。
from tensorflow.keras.layers import ( Conv2D, BatchNormalization, GlobalAveragePooling2D, Reshape, UpSampling2D, Concatenate, ReLU ) def conv_bn_relu(x, filters, kernel_size=1, dilation_rate=1): x = Conv2D(filters, kernel_size, padding='same', dilation_rate=dilation_rate, use_bias=False)(x) x = BatchNormalization()(x) x = ReLU()(x) return x def aspp_block(inputs, filters=256, rates=(6, 12, 18)): # 1x1卷积分支 branch1 = conv_bn_relu(inputs, filters, 1) # 三个不同膨胀率的3x3空洞卷积 branch2 = conv_bn_relu(inputs, filters, 3, rates[0]) branch3 = conv_bn_relu(inputs, filters, 3, rates[1]) branch4 = conv_bn_relu(inputs, filters, 3, rates[2]) # 全局平均池化分支 pool = GlobalAveragePooling2D()(inputs) pool = Reshape((1, 1, -1))(pool) pool = conv_bn_relu(pool, filters, 1) pool = UpSampling2D(size=(inputs.shape[1], inputs.shape[2]), interpolation='bilinear')(pool) x = Concatenate()([branch1, branch2, branch3, branch4, pool]) x = conv_bn_relu(x, filters, 1) return x实现时有一个关键注意点:全局平均池化分支必须上采样回输入特征图一样的尺寸。因为后面的Concatenate要求所有分支的分辨率一致。有些教程省略了这一步,直接拼接,代码会报形状错误。
4.3 骨干网络选型:ResNet-50还是MobileNetV2
DeepLab V3+原论文用的是Xception作为骨干网络,但在实际工程中,Xception的预训练权重难找,训练又慢。我的项目里做了两个版本的对比:ResNet-50和MobileNetV2。
ResNet-50版本精度最高,参数也最大,模型体积约41MB。MobileNetV2版本的模型只有不到6MB,推理速度几乎快了一倍,但精度会低一些。如果你做的是学术研究或者对精度要求极高,选ResNet-50;如果后面要落地到Web端或移动端,MobileNetV2是更务实的选择。
另外一个重要的点是:遥感图像的分布和ImageNet差别很大,骨干网络的预训练权重只能作为初始化起点,不能指望它直接适合遥感场景。我在实验中发现,用ImageNet预训练权重初始化的MobileNetV2,在遥感数据上需要更多的训练轮次才能完全适应,前期loss下降比ResNet-50慢不少。
5. 训练参数与损失函数调优实战
5.1 类别不平衡处理:从交叉熵到组合损失
训练一开始,我用的是常规的交叉熵损失函数。跑了几个epoch后发现训练集loss降得很快,但验证集mIoU一直上不去。看预测结果才发现,模型把几乎所有的像素都预测成了背景,因为建筑物在整张图里占比太小,全预测为背景就已经能拿到95%以上的准确率。
这就是典型的类别不平衡问题。解决办法有很多,我这边用了两个组合策略。
第一,使用Dice Loss + CrossEntropy的组合损失。Dice Loss直接优化目标区域的重叠度,对前景和背景比例不敏感,非常适合小目标分割。我的实现如下:
import tensorflow as tf def dice_coef(y_true, y_pred, smooth=1e-5): y_true_f = tf.reshape(y_true, [-1]) y_pred_f = tf.reshape(y_pred, [-1]) intersection = tf.reduce_sum(y_true_f * y_pred_f) return (2.0 * intersection + smooth) / ( tf.reduce_sum(y_true_f) + tf.reduce_sum(y_pred_f) + smooth ) def dice_loss(y_true, y_pred): return 1.0 - dice_coef(y_true, y_pred) def combined_loss(y_true, y_pred): ce = tf.keras.losses.CategoricalCrossentropy()(y_true, y_pred) dice = dice_loss(y_true, y_pred) return 0.6 * ce + 0.4 * dice第二,配合做了前景区域加权采样。在生成Dataset的时候,统计每个切片的掩码中前景像素占比,对前景占比高于阈值的切片提高采样概率。这个策略不需要改模型,对提升小目标分割效果立竿见影。
5.2 学习率策略与优化器选择
优化器方面,我最初用Adam,虽然收敛快,但最后阶段精度上不去。后来把两个模型的学习率策略统一成了余弦退火 + Warm-up,精度都有一定提升。
在没有现成Warm-up回调的情况下,直接用Keras的ReduceLROnPlateau是最省事的方案。我设置的是:监控验证集mIoU,连续5个epoch不上升就把学习率乘以0.5,最低降到1e-6。配合训练100轮,实际用了大概65轮就收敛了。
callbacks = [ tf.keras.callbacks.ModelCheckpoint( 'weights/deeplab_best.h5', save_best_only=True, monitor='val_miou', mode='max' ), tf.keras.callbacks.ReduceLROnPlateau( monitor='val_miou', factor=0.5, patience=5, min_lr=1e-6 ), tf.keras.callbacks.EarlyStopping( patience=15, restore_best_weights=True ) ]5.3 过拟合与训练曲线诊断
遥感图像分割的过拟合,常常比分类任务更隐蔽。分类任务过拟合表现为准确率到顶后开始下降,而分割任务过拟合前期往往看不出明显症状,训练集mIoU一直很高,验证集mIoU在一个区间震荡,就是不涨。
我排查过拟合用了一个很土但有效的方法:拿训练好的模型去跑训练集里的样本,看预测掩码跟标签是不是完全贴合。如果训练集上表现完美,但验证集掉点,那大概率是过拟合。这时候优先调整三点:减弱数据增强强度,增加Dropout,或者降低模型容量。
还有一种常见情况是验证集loss震荡剧烈,通常不是过拟合,而是验证集本身太小、样本不均衡。我这边是让验证集至少保留15%的切片,并且保证各类别地物的占比跟整体分布一致,这样指标才有参考价值。
6. 模型评估与对比实验分析
6.1 评估指标的选择与计算
语义分割的评估指标,最常用的是mIoU(平均交并比)和Pixel Accuracy。mIoU计算的是每个类别的预测区域和真实区域的交集除以并集,再对类别取平均。公式如下:
mIoU = (1 / N) * sum( TP_i / (TP_i + FP_i + FN_i) )我实现了一个TensorFlow版本的mIoU,方便在训练过程中直接作为监控指标:
class MiouMetric(tf.keras.metrics.Metric): def __init__(self, num_classes=2, name='miou', **kwargs): super().__init__(name=name, **kwargs) self.num_classes = num_classes self.intersection = self.add_weight(name='inter', initializer='zeros') self.union = self.add_weight(name='union', initializer='zeros') def update_state(self, y_true, y_pred, sample_weight=None): y_pred = tf.argmax(y_pred, axis=-1) y_true = tf.argmax(y_true, axis=-1) for cls in range(self.num_classes): pred_mask = (y_pred == cls) true_mask = (y_true == cls) inter = tf.reduce_sum(tf.cast(tf.logical_and(pred_mask, true_mask), tf.float32)) union = tf.reduce_sum(tf.cast(tf.logical_or(pred_mask, true_mask), tf.float32)) self.intersection.assign_add(inter) self.union.assign_add(union) def result(self): return self.intersection / (self.union + 1e-7)这里有个细节:循环计算每个类别的TP、FP、FN时,如果数据类别特别多(比如几十类),用向量化运算更好,我这里只有两类,循环写起来方便,性能也能接受。
6.2 UNet与DeepLab V3+的实测对比结果
在同样的数据集、同样的训练策略下,最终结果如下:
| 模型 | mIoU | Pixel Accuracy | 参数大小 | 单张推理时间 |
|---|---|---|---|---|
| UNet | 0.721 | 0.963 | 31MB | 45ms |
| DeepLab V3+ (ResNet-50) | 0.758 | 0.971 | 41MB | 70ms |
| DeepLab V3+ (MobileNetV2) | 0.726 | 0.964 | 5.8MB | 32ms |
结果符合预期,但并不意外。DeepLab V3+(ResNet-50)在各指标上都领先,尤其是mIoU比UNet高出近4个百分点。直观上看,DeepLab V3+在大尺度地物和复杂背景区域的表现更好;UNet则在一些小目标的边缘细节上更紧凑,但误检率稍高。
MobileNetV2版本的DeepLab V3+,精度跟UNet基本持平,但参数和推理速度优势非常明显。这个对比说明,如果产品对实时性要求高,MobileNetV2是性价比最高的选择。
6.3 预测结果可视化与后处理
训练完成后,不能直接拿原始分割结果交差。模型输出的掩码图通常存在两类问题:一是零散的噪点,比如一个孤立的像素被误判为建筑物;二是建筑物边缘存在锯齿和不连续。我这边加了后处理步骤,用OpenCV做形态学闭运算和小连通域剔除:
import cv2 import numpy as np def postprocess(pred_mask, min_area=100): # pred_mask 是一张0/1的单通道二值图 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) mask = cv2.morphologyEx(pred_mask, cv2.MORPH_CLOSE, kernel) # 删除面积小于阈值的连通域 num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8) cleaned = np.zeros_like(mask) for i in range(1, num_labels): if stats[i, cv2.CC_STAT_AREA] >= min_area: cleaned[labels == i] = 1 return cleaned这一个后处理步骤,单看mIoU能提升0.5到1个百分点。虽说不算多,但在实际交付里,减少了大量肉眼可见的误检区。
7. 常见问题与排查技巧实录
7.1 显存溢出(OOM)怎么办
训练时最容易遇到的就是显存溢出,最常见原因是输入尺寸太大、batch size太大、或者模型参数太多。我的处理顺序是:先把batch size从8降到4;如果还溢出,把输入尺寸从512降到384或者256;再不行,换参数量更小的骨干网络。
另外一个技巧是开启混合精度训练。TensorFlow里加两行配置,显存占用能减少30%左右,速度还有提升。对遥感影像这种大输入分辨率场景很实用。
from tensorflow.keras import mixed_precision mixed_precision.set_global_policy('mixed_float16')7.2 训练loss下降缓慢甚至不降
这种情况我遇到过两次。第一次是学习率问题,Adam初始学习率用了1e-2,loss直接飞出天际。第二次是数据问题,Dataset里图片和掩码没对齐,模型相当于在学习一张图配另一张图的错误标签。
排查步骤:先用几十张数据过拟合看看,如果几轮epoch后loss能降下来,说明模型和代码没问题,问数据;如果loss纹丝不动,检查数据管线、归一化、One-Hot编码,从头捋。
7.3 预测结果出现棋盘格状伪影
棋盘格状伪影(checkerboard artifacts)通常出现在用转置卷积做上采样的模型中。严格说,这不是UNet独有的问题,而是转置卷积的常见问题。如果这种情况严重影响视觉效果,可以把上采样方式从转置卷积换成UpSampling2D + Conv2D,也就是先插值再卷积。
我实际对比过,Conv2DTranspose在小数据集上更容易产生棋盘格,而UpSampling2D + Conv2D生成的结果更平滑。代价是边缘稍微柔和一些,但对遥感建筑物提取影响不大。介意精度损失的话,保持转置卷积也没问题。
7.4 分割结果有大量盐和胡椒噪声
如果预测结果上散布着密密麻麻的小噪点,多到肉眼可见,通常说明模型对局部区域不自信,或者输入切片本身存在大量重叠导致同一个目标在不同切片上预测结果不一致。
重叠区域的问题可以在推理阶段解决:对重叠切片赋予非均匀的权重,只有中心区域的计算结果参与最终预测,边缘区域的结果直接丢弃。这种方法叫overlap-tile策略,实现起来不难,但能明显提升拼接区域的一致性。这也是我在做整幅大图推理时的默认做法。
7.5 验证集指标高但实际效果差
最后提醒一个容易被忽略的问题。有些人训练时验证集mIoU看起来很高,但把模型放到新的遥感影像上一测,效果很差。这种问题往往不是模型本身的问题,而是数据集划分的问题。
遥感影像中,相邻切片之间有很强的空间相关性。如果训练集和验证集的切片来自同一张原始大图,那验证集指标虚高。正确的做法是按原始影像来划分,比如以影像文件为最小单位,一部分原始影像整体划分给训练集,另一部分整体划给验证集。这样模型在验证集上的表现才更接近真实场景的泛化能力。
我在项目里一开始就是因为没注意这点,导致验证集mIoU虚高了5个百分点,后来重新划分数据后才得到可靠的指标。这种数据泄漏问题在遥感任务里特别容易踩,因为相邻切片看起来像是不同样本,实际上语义内容高度重叠。
本文还有配套的精品资源,点击获取