1. 从零开始:为什么选择DeeplabV3+和GID数据集?

如果你刚接触遥感深度学习,面对五花八门的模型和数据集,可能会有点懵。我当时也一样,想找一个既能快速上手、效果又足够好的实战项目。折腾了一圈,最后锁定了DeeplabV3+和GID数据集这个组合。为什么是它俩?简单说,就是“强强联合,对新手友好”。

先说模型。DeeplabV3+在图像分割领域是个明星选手,它不是凭空冒出来的,而是在前几代基础上,专门针对“边界模糊”、“小目标识别难”这些痛点做了优化。你想想看,遥感影像里,农田和草地的边界、城市里密集的小型建筑,用普通模型去分,边缘处经常糊成一片。DeeplabV3+的杀手锏有两个:一个是空洞空间金字塔池化(ASPP),另一个是编码器-解码器结构。ASPP就像你用不同倍率的放大镜同时观察同一张图片,有的看全局轮廓,有的看局部细节,然后把看到的信息综合起来,这样无论地物是大是小,模型都能“看”得更清楚。编码器-解码器结构则负责“精修”,编码器把图片信息高度压缩、提炼出核心特征,解码器再一步步把这个抽象特征还原成一张清晰的分割图,专门修复边缘,让分类结果不再毛毛糙糙。而且,它用了深度可分离卷积,大大减少了计算量,这意味着我们用普通的显卡也能跑起来,不用眼巴巴望着那些顶级设备。

再说数据。做深度学习,七分靠数据,三分靠调参。GID数据集简直就是为遥感土地覆盖分类量身定做的“教科书”。它来自咱们国家的高分二号卫星,分辨率非常高,地面上0.8米的东西都能看清楚,地物细节非常丰富。它提供了两个版本:GID-5和GID-15。GID-5包含建筑、农田、森林、草地、水域这5个大类,有150景大图,数据量足够我们做充分的训练和验证,非常适合入门,快速验证流程和模型基线性能。等你把五大类玩熟了,就可以挑战GID-15,它把地物分得更细,比如农田会细分成灌溉地、旱地、果园等,建筑会细分成城市住宅、工业厂房等,一共15类。这就能解决更实际、更精细的业务需求。数据标注质量也很高,是专家们手工精标的,可靠性有保障,避免了“垃圾进,垃圾出”的尴尬。

所以,这个组合给我的感觉就是:DeeplabV3+提供了强大且高效的“大脑”,GID提供了高质量且标准的“教材”。用这个组合入门,你能把数据准备、模型训练、评估优化的完整流程走通,获得的经验可以直接迁移到其他遥感任务上,性价比极高。我当初就是看中了这一点,实测下来,这个选择确实很稳。

2. 实战第一步:搞定GID数据集与环境配置

理论说再多,不如动手做一遍。咱们的第一步,就是把“教材”准备好,并把“实验环境”搭起来。这一步会碰到一些坑,我把自己踩过的和解决方法都列出来,你照着做应该能顺利过关。

2.1 GID数据集的下载与解压

首先,你得去GID的官方发布页面下载数据。这里有个小细节,官方提供的压缩包解压后,里面通常包含三个文件夹,新手很容易搞混:

  • GID-15_10: 这个里面是10景完整的GID-15数据(注意,是“景”,不是“张”,一景很大)。
  • GID-5_150: 这个里面是150景GID-5的数据。
  • GID-5_label_150: 这个里面是GID-5数据对应的标注文件(PNG格式)。

我一开始就以为GID-5_150里自带标注,结果训练时一直报错找不到标签。所以切记,GID-5的影像和标签是分开存放的。下载后,建议你建立一个清晰的项目目录,比如这样:

GID_Project/
├── data/
│   ├── GID-5_150/          # 存放GID-5影像
│   ├── GID-5_label_150/    # 存放GID-5标签
│   └── GID-15_10/          # 存放GID-15数据(影像和标签通常在一起)
├── scripts/                # 存放数据处理的Python脚本
├── checkpoints/            # 存放训练好的模型
└── results/                # 存放预测结果和可视化图

数据解压后,你会发现影像文件是TIFF格式,标签是PNG格式。TIFF文件可能包含多个波段(如红、绿、蓝、近红外),我们需要用代码读取并处理。标签PNG是单通道的,每个像素点的值代表类别索引(例如,0代表背景,1代表建筑,2代表农田等)。这个对应关系一定要去官方文档里确认清楚,不同版本可能会有细微差别。

2.2 数据处理与数据集构建

原始数据太大,不能直接扔给模型。我们需要把它切成固定大小的小块(比如512x512),并划分训练集、验证集和测试集。这里我分享一个实用的处理脚本的核心部分:

import os
import numpy as np
from PIL import Image
import tifffile as tiff

def crop_image_and_label(img_path, label_path, crop_size=512, stride=256, save_dir='./patches'):
    """
    将大图影像和标签裁剪成小块
    """
    os.makedirs(save_dir, exist_ok=True)
    img = tiff.imread(img_path)  # 读取多波段TIFF
    label = np.array(Image.open(label_path)) # 读取PNG标签

    h, w = img.shape[:2]
    count = 0

    for i in range(0, h - crop_size + 1, stride):
        for j in range(0, w - crop_size + 1, stride):
            img_patch = img[i:i+crop_size, j:j+crop_size, :]
            label_patch = label[i:i+crop_size, j:j+crop_size]

            # 简单过滤掉标签全是背景的图块,节省空间
            if np.sum(label_patch) > 0:
                np.save(os.path.join(save_dir, f'img_{count}.npy'), img_patch)
                np.save(os.path.join(save_dir, f'label_{count}.npy'), label_patch)
                count += 1
    print(f'生成 {count} 个有效图块。')

这个函数实现了滑动窗口裁剪,并跳过了全是背景的无效块。处理完所有影像后,你会得到成千上万个小图块。接下来,你需要用另一个脚本,把这些图块的文件路径列表随机打乱,然后按8:1:1的比例分成训练集、验证集和测试集,并生成三个文本文件(train.txt, val.txt, test.txt)记录路径。这样,后面写数据加载器时,直接读这些列表文件就行了。

2.3 深度学习环境搭建

模型训练我推荐用PyTorch,生态好,代码灵活。环境配置其实不复杂,主要是版本要对上。下面是我常用的一个环境配置命令(以conda为例):

conda create -n rs_deeplab python=3.8
conda activate rs_deeplab
pip install torch==1.12.1+cu113 torchvision==0.13.1+cu113 --extra-index-url https://download.pytorch.org/whl/cu113
pip install opencv-python pillow tifffile scikit-learn matplotlib tensorboard

这里注意,torch和torchvision的版本要匹配,并且cu113指的是CUDA 11.3,你需要根据自己显卡的CUDA版本进行调整。装完后,写个简单的测试脚本,确认GPU可以被PyTorch调用:

import torch
print(torch.__version__)
print(torch.cuda.is_available())
print(torch.cuda.get_device_name(0))

如果都能正确输出,恭喜你,硬件环境就妥了。接下来,我们需要自己实现一下DeeplabV3+的数据加载器,因为我们要处理的是多波段的遥感影像,和普通的RGB三通道图片不一样。

3. 模型核心:动手实现与理解DeeplabV3+

网上有很多现成的DeeplabV3+代码,但我强烈建议你至少跟着实现一遍核心部分,这能帮你真正理解它为什么强。我们基于PyTorch来搭建。

3.1 搭建编码器与ASPP模块

编码器通常选用在ImageNet上预训练好的骨干网络,比如ResNet-50或ResNet-101。预训练权重能提供非常好的初始特征提取能力,这对遥感任务同样有效,是一种高效的迁移学习。我们不是从头搭建ResNet,而是利用torchvision.models里的预训练模型,提取其中我们需要的中层和深层特征。

ASPP模块是DeeplabV3+的灵魂,它的实现非常巧妙:

import torch
import torch.nn as nn
import torch.nn.functional as F

class ASPP(nn.Module):
    def __init__(self, in_channels, out_channels=256):
        super(ASPP, self).__init__()
        # 1x1卷积
        self.conv1 = nn.Conv2d(in_channels, out_channels, 1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        # 三个不同空洞率的3x3卷积
        self.conv2 = nn.Conv2d(in_channels, out_channels, 3, padding=6, dilation=6, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.conv3 = nn.Conv2d(in_channels, out_channels, 3, padding=12, dilation=12, bias=False)
        self.bn3 = nn.BatchNorm2d(out_channels)
        self.conv4 = nn.Conv2d(in_channels, out_channels, 3, padding=18, dilation=18, bias=False)
        self.bn4 = nn.BatchNorm2d(out_channels)
        # 全局平均池化 + 卷积
        self.global_avg_pool = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),
            nn.Conv2d(in_channels, out_channels, 1, bias=False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU()
        )
        # 融合后的卷积
        self.conv_final = nn.Conv2d(out_channels*5, out_channels, 1, bias=False)
        self.bn_final = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU()

    def forward(self, x):
        x1 = self.relu(self.bn1(self.conv1(x)))
        x2 = self.relu(self.bn2(self.conv2(x)))
        x3 = self.relu(self.bn3(self.conv3(x)))
        x4 = self.relu(self.bn4(self.conv4(x)))
        x5 = self.global_avg_pool(x)
        # 将全局特征上采样到和其他特征一样的大小
        x5 = F.interpolate(x5, size=x.size()[2:], mode='bilinear', align_corners=True)
        # 在通道维度上拼接
        x = torch.cat((x1, x2, x3, x4, x5), dim=1)
        x = self.relu(self.bn_final(self.conv_final(x)))
        return x

这段代码构建了一个完整的ASPP模块。它并行使用了5条支路:1个1x1卷积、3个不同空洞率的3x3卷积(感受野依次增大)、1个全局平均池化。最后把5条路得到的特征图在通道维度拼接起来,再用一个1x1卷积融合。这样,输出的特征图就同时包含了局部细节、多尺度上下文和全局信息。

3.2 构建完整的DeeplabV3+网络

有了ASPP,我们就可以组装完整的模型了。模型分为编码器、ASPP、解码器三部分。

class DeepLabV3Plus(nn.Module):
    def __init__(self, backbone='resnet50', num_classes=5):
        super(DeepLabV3Plus, self).__init__()
        # 1. 编码器部分:加载预训练ResNet,并提取中间层和输出层
        if backbone == 'resnet50':
            resnet = torchvision.models.resnet50(pretrained=True)
        else:
            resnet = torchvision.models.resnet101(pretrained=True)

        # 提取ResNet的中间特征(用于解码器)和最终特征(用于ASPP)
        self.layer0 = nn.Sequential(resnet.conv1, resnet.bn1, resnet.relu, resnet.maxpool)
        self.layer1 = resnet.layer1 # 输出通道256
        self.layer2 = resnet.layer2 # 输出通道512
        self.layer3 = resnet.layer3 # 输出通道1024
        self.layer4 = resnet.layer4 # 输出通道2048

        # 2. ASPP模块,输入通道为2048
        self.aspp = ASPP(in_channels=2048, out_channels=256)

        # 3. 解码器部分
        # 对低层特征(layer1输出)进行卷积,减少通道数
        self.low_level_conv = nn.Sequential(
            nn.Conv2d(256, 48, 1, bias=False),
            nn.BatchNorm2d(48),
            nn.ReLU()
        )
        # 融合后的卷积
        self.decoder_conv = nn.Sequential(
            nn.Conv2d(256+48, 256, 3, padding=1, bias=False),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            nn.Conv2d(256, 256, 3, padding=1, bias=False),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            nn.Conv2d(256, num_classes, 1) # 输出分类数
        )

    def forward(self, x):
        # 编码器前向传播
        x_low = self.layer0(x)   # 初始下采样
        x_low = self.layer1(x_low) # 保存此输出,用于后续解码器
        x = self.layer2(x_low)
        x = self.layer3(x)
        x_high = self.layer4(x)  # 高层语义特征

        # ASPP处理高层特征
        x_aspp = self.aspp(x_high)

        # 解码器
        # 1. 对ASPP输出上采样4倍
        x_aspp_up = F.interpolate(x_aspp, size=x_low.shape[2:], mode='bilinear', align_corners=True)
        # 2. 处理低层特征
        x_low_reduced = self.low_level_conv(x_low)
        # 3. 拼接
        x_decoder = torch.cat([x_aspp_up, x_low_reduced], dim=1)
        # 4. 卷积得到最终输出
        output = self.decoder_conv(x_decoder)
        # 5. 上采样到输入图像大小
        output = F.interpolate(output, size=x.shape[2:], mode='bilinear', align_corners=True)

        return output

这个实现清晰地展示了数据流向:输入图片经过编码器(ResNet)层层下采样,得到富含语义的高层特征,送入ASPP捕获多尺度信息。同时,编码器早期(layer1)的特征被保留,它包含丰富的空间细节。在解码器,我们将ASPP输出的特征上采样,与细节丰富的低层特征拼接,再经过几个卷积层,最终上采样回原图尺寸,得到每个像素的类别预测。这个“高层语义指导,低层细节修复”的过程,正是DeeplabV3+保持边缘精度的关键。

4. 训练与调优:让模型在GID数据上“学好”

模型搭好了,数据也准备好了,接下来就是最关键的训练环节。这个过程不是简单地跑起来就完事,里面有很多技巧和坑。

4.1 损失函数与评估指标的选择

土地覆盖分类是典型的像素级分类任务,常用的损失函数是交叉熵损失。但由于GID数据中各类别像素数量可能不均衡(比如“水域”可能比“建筑”少很多),直接使用标准交叉熵会导致模型偏向于大类。我试过几种方案:

  1. 带权重的交叉熵损失:根据每个类别在训练集中的像素占比,为其计算一个权重,占比越小,权重越大。这样模型会对小类别的错误给予更多“惩罚”。
  2. Dice Loss:特别适用于分割任务,直接优化预测区域和真实区域的重叠度,对类别不均衡有一定鲁棒性。 我通常会把两者结合起来,用加权交叉熵损失 + Dice Loss的混合损失,效果往往比单用好。
class HybridLoss(nn.Module):
    def __init__(self, weight=None):
        super().__init__()
        self.ce_loss = nn.CrossEntropyLoss(weight=weight)
        # Dice Loss需要自己实现
    def dice_loss(self, pred, target):
        smooth = 1.
        pred = F.softmax(pred, dim=1)
        # 将预测转为one-hot形式计算
        # ... 具体实现省略
        return 1 - dice_coeff

    def forward(self, pred, target):
        ce = self.ce_loss(pred, target)
        dice = self.dice_loss(pred, target)
        return ce + dice

评估指标不能只看整体准确率,那样会掩盖模型在小类别上的糟糕表现。必须看类别平均交并比(Mean IoU)和频权交并比(Frequency Weighted IoU)。IoU就是预测区域和真实区域交集与并集的比值,对每个类别单独计算,然后取平均。这能真实反映模型对每一类地物的分割能力。我习惯在验证集上每训练一个epoch就计算一次mIoU,并保存最好的模型。

4.2 训练策略与关键参数设置

训练参数设置直接影响收敛速度和最终效果。以下是我经过多次实验后,觉得比较靠谱的一套配置:

  • 优化器:使用AdamW。它比传统的Adam优化器加入了权重衰减的正则化,能有效防止过拟合。初始学习率设为1e-4。
  • 学习率调度:使用余弦退火(Cosine Annealing)或者带热重启的余弦退火。这能让学习率在训练过程中平滑下降,并在后期进行小幅“重启”,有助于模型跳出局部最优。我用torch.optim.lr_scheduler.CosineAnnealingLR。
  • Batch Size:在显存允许的情况下尽量设大,比如8或16。大的Batch Size能使梯度更新更稳定。
  • Epoch数:对于GID-5,100-150个epoch通常足够了;对于更复杂的GID-15,可能需要200个epoch以上。要配合早停(Early Stopping)策略,比如连续10个epoch验证集mIoU不提升就停止。

一个训练循环的核心代码结构如下:

model = DeepLabV3Plus(num_classes=5).cuda()
criterion = HybridLoss(weight=class_weights).cuda()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100)

best_miou = 0.0
for epoch in range(num_epochs):
    model.train()
    for images, labels in train_loader:
        images, labels = images.cuda(), labels.cuda()
        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
    scheduler.step()

    # 验证阶段
    model.eval()
    with torch.no_grad():
        # 计算验证集上的mIoU
        current_miou = evaluate_on_val(val_loader, model)
        if current_miou > best_miou:
            best_miou = current_miou
            torch.save(model.state_dict(), f'best_model.pth')
            print(f'保存最佳模型,mIoU: {best_miou:.4f}')

4.3 针对遥感影像的调优技巧

  1. 数据增强:遥感影像的数据增强除了常见的随机水平/垂直翻转、旋转外,还可以加入随机亮度、对比度调整,模拟不同光照条件;加入随机裁剪并缩放,增加尺度多样性。注意,进行几何变换时,影像和标签必须同步变换。
  2. 多尺度训练:在训练时,每次迭代随机将输入图像缩放到一个预设的尺度范围(如0.5倍到2.0倍),这能极大地提升模型对不同尺寸地物的识别能力。
  3. 注意力机制尝试:在ASPP之后或解码器中,可以尝试加入CBAM或SE等注意力模块,让模型更关注重要的地物区域。不过这会增加计算量,需要权衡。
  4. 后处理:模型预测出的分割图可能会有一些细小的空洞或孤立的噪点。可以使用条件随机场(CRF)或简单的形态学开闭运算进行后处理,平滑边界,去除小噪声,这能直观地提升视觉效果和指标。

5. 结果分析与可视化:看看模型到底学得怎么样

模型训练完了,保存了最佳权重,但这还没结束。我们必须深入分析结果,知道模型哪里做得好,哪里不行,才能有的放矢地改进。

5.1 定量评估与错误分析

首先,在独立的测试集上运行模型,计算最终的mIoU、各类别IoU、总体精度等指标。建议做成一个清晰的表格:

类别IoU (%)像素准确率 (%)备注
背景99.299.5通常很高
建筑85.688.3边界处易与道路混淆
农田78.982.1内部纹理复杂,易错分
森林92.393.7表现最好
水域90.191.5形状规则时很好
平均 (mIoU)89.2--

从这样的表格里,你能一眼看出模型的短板。比如上表中“农田”的IoU最低。接下来就要做错误分析:找出那些被分错的农田样本,可视化看看。你会发现,可能有些农田被错分成了“草地”(因为颜色纹理相近),或者农田的边缘被模糊了。这些具体的观察,就是你下一步改进的方向——比如,可以针对性地增加这两类难分样本的数据增强,或者在损失函数中给“农田”和“草地”更高的权重。

5.2 预测结果可视化

数字指标是冰冷的,可视化结果才是直观的。我们可以用Python的Matplotlib把原始影像、真实标签和模型预测放在一起对比。

def visualize_prediction(image, label, pred, save_path):
    """
    image: 原始影像 (H, W, C)
    label: 真实标签 (H, W)
    pred: 模型预测 (H, W)
    """
    fig, axes = plt.subplots(1, 3, figsize=(15, 5))
    axes[0].imshow(image[:, :, [2,1,0]]) # 显示RGB三波段
    axes[0].set_title('原始影像')
    axes[0].axis('off')

    axes[1].imshow(label, cmap='tab20c') # 使用分类colormap
    axes[1].set_title('真实标签')
    axes[1].axis('off')

    axes[2].imshow(pred, cmap='tab20c')
    axes[2].set_title('模型预测')
    axes[2].axis('off')

    plt.savefig(save_path, dpi=300, bbox_inches='tight')
    plt.close()

通过大量可视化,你不仅能确认模型在大部分区域工作良好,更能精准定位问题。例如,你可能会发现:

  • 大块均质区域(如大片水域、森林)分类非常准确。
  • 建筑物边缘比传统模型清晰很多,这得益于解码器结构。
  • 阴影下的地物(如背阴的建筑侧面)容易错分,这是因为光谱信息发生了变化。
  • 不同类别交界处仍然存在少量混淆,尤其是光谱特征相似的类别(如灌溉农田和草地)。

5.3 从GID-5到GID-15的挑战与迁移

当你在GID-5上取得不错的结果后,可以尝试挑战更精细的GID-15分类。这里有个小技巧:不要从头开始训练。用你在GID-5上训练好的模型权重作为预训练模型,然后只在GID-15数据上对最后的分类头进行微调(Fine-tuning),或者以较低的学习率微调整个网络。这是因为模型在GID-5上学到的如何提取边缘、纹理等低级特征,以及如何理解“建筑”、“植被”等高级语义,这些知识对GID-15任务仍然是通用的。迁移学习能极大加快收敛速度,并提升最终精度。

在实际操作中,你需要修改模型的输出类别数(从5改为15),然后加载之前训练好的权重(注意跳过分类头权重不匹配的部分)。在训练时,可以冻结编码器和ASPP部分的前面几层,只训练后面的层和解码器,这样既能利用已有知识,又能适应新任务。

整个项目走下来,从数据准备到模型调优,再到结果分析,你会对遥感深度学习的全流程有一个非常扎实的把握。最重要的是,你获得了一套可以复用的方法论和代码框架,以后无论面对新的数据集还是新的分割模型,你都知道该从哪里入手,如何去实验和优化。这比单纯跑通一个代码,价值要大得多。我在处理自己课题的遥感数据时,这套流程帮我节省了大量摸索时间,希望对你也有帮助。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐