1. 项目缘起与核心问题拆解
遥感影像的自动解译这些年一直是个热门方向,但真正动手做过的人都知道,坑远比想象中多。我最早接触这个领域是在一个区域土地覆盖普查的辅助项目里,当时手头有一批亚米级的高分辨率影像,分辨率能到0.5米左右,理论上地物细节非常丰富,但实际标注数据少得可怜——整片区域能拿到的可靠标注样本只有几千个像素级标签,而且类别分布极度不均衡,建设用地和水体占了七成以上,剩下的林地、耕地、裸地都是零散小块。这种场景下,如果从零训练一个深度网络,基本等于自杀,模型会严重过拟合到多数类上。
这个项目的核心命题就是:在高分辨率遥感影像上,用可迁移的深度模型完成土地覆盖分类。说白了,就是借助在大规模自然图像数据集上预训练好的网络,把学到的通用特征迁移到遥感场景里,再通过少量标注样本做微调,最终输出每个像素的类别标签。它解决的是遥感领域最典型的两个痛点:一是标注成本极高,二是不同传感器、不同区域、不同季节的影像分布差异大,模型泛化能力差。
适合谁来参考这篇内容?如果你是从业者,手头有遥感影像分类任务但标注数据有限,或者你正在做多源数据迁移学习,那这篇东西应该能帮你少走弯路。如果你是对深度学习落地感兴趣的学生或工程师,想了解迁移学习在真实场景里怎么调、怎么踩坑,也可以顺着往下看。我会把整个流程拆开,从数据准备、模型选型、迁移策略、训练细节到问题排查,尽量讲透。
2. 整体方案设计与迁移逻辑
2.1 为什么选迁移学习而不是从零训练
先算一笔账。假设你要从零训练一个ResNet-50级别的网络做像素级分类,至少需要多少标注?按照经验,每个类别没有几千到上万个独立样本,网络根本学不到稳定特征。而遥感影像的像素级标注,一个熟练的标注员一天能标完的干净区域可能就几平方公里,换算成像素也就百万级别,但这里面有效类别样本可能只有几万。更麻烦的是,遥感影像的类内差异极大——同样是水体,深水区和浅水区、清水和浊水的光谱响应完全不同;同样是建设用地,密集城区和工业园区的纹理差异也很大。从零训练的网络很容易把这些差异当成不同类别来学。
迁移学习的逻辑就完全不一样了。像ImageNet这种大规模自然图像数据集,包含了一千多万张标注图像,网络在训练过程中已经学到了边缘、纹理、颜色梯度、局部形状等底层和中层特征。这些特征对遥感影像同样有效,因为地物的边界、纹理、光谱模式在底层视觉特征上是相通的。你要做的只是让网络学会把这些通用特征映射到遥感类别上,而不是重新发明一套特征提取器。
注意:迁移学习不是万能药。如果源域(自然图像)和目标域(遥感影像)的差异过大,比如源域全是室内场景,目标域是农田,那底层特征可能就不太匹配。但ImageNet的多样性足够好,覆盖了天空、水面、植被、建筑等大量与遥感重叠的语义,所以实际效果通常很稳。
2.2 可迁移深度模型的选型考量
选什么骨干网络,直接决定了迁移效果和计算成本。我试过几种主流方案,这里把对比列出来,方便你根据自己手头的算力和数据量做选择。
| 骨干网络 | 参数量 | 预训练数据 | 迁移到遥感的适配难度 | 适用场景 |
|---|---|---|---|---|
| ResNet-50 | 25M | ImageNet | 低,特征通用性强 | 中等数据量,算力有限 |
| ResNet-101 | 44M | ImageNet | 低,但训练更慢 | 数据量较大,追求精度 |
| EfficientNet-B3 | 12M | ImageNet | 中,需要调输入分辨率 | 算力紧张,移动端部署 |
| ViT-Base | 86M | ImageNet-21k | 高,需要更多数据微调 | 大数据量,追求SOTA |
| Swin-T | 28M | ImageNet | 中,窗口注意力对遥感友好 | 高分辨率输入,细节丰富 |
我最终选的是ResNet-50作为基线,原因很实际:第一,它的预训练权重最容易拿到,社区支持好;第二,25M参数量在单卡上就能跑,不用折腾分布式;第三,它的特征金字塔结构天然适合做多尺度融合,而遥感影像里地物尺度变化极大,从几米宽的小路到几公里的大型农田,多尺度信息非常关键。如果你手头数据量超过十万级像素样本,可以上ResNet-101或者Swin-T,精度会有提升,但训练时间大概翻倍。
2.3 迁移策略的分层设计
迁移不是简单地把预训练权重加载进来就完事了。网络的不同层学到的东西不一样,底层学的是边缘、角点、颜色这些通用特征,高层学的是语义类别相关的抽象特征。遥感影像和自然图像在底层特征上高度相似,但在高层语义上差异明显。所以我的策略是分层设置学习率:底层用极小的学习率甚至冻结,中层用中等学习率,高层用正常学习率。
具体来说,ResNet-50的conv1到layer2我直接冻结,学习率设为0;layer3用1e-4的学习率;layer4和后面的分类头用1e-3的学习率。这样做的逻辑是:底层特征不需要改,改了反而破坏通用性;中层特征需要轻微调整以适应遥感纹理;高层特征需要大幅调整来适配新的类别定义。实测下来,这种分层策略比全局统一学习率收敛快至少30%,而且最终精度高2到3个百分点。
3. 数据准备与预处理的关键细节
3.1 高分辨率影像的读取与分块
高分辨率遥感影像动辄几个GB,直接塞进内存不现实。我的做法是先把大图切成512x512的图块,重叠率设成25%,也就是步长384。为什么要重叠?因为如果直接硬切,地物边界正好落在切块边缘时,上下文信息就丢了,分类结果会出现明显的拼接缝。25%的重叠能保证边界区域至少出现在两个图块里,后续做推理时可以把重叠区域的预测结果做平均,消除拼接痕迹。
切块的时候要注意,遥感影像的坐标参考系和普通图像不一样,切块后要记录每个图块在原图中的行列号,方便后续拼接回完整分类图。我一般用一个简单的CSV文件记录:图块文件名、起始行、起始列、宽度、高度。这个文件在最后出图的时候是刚需,千万别省。
import rasterio from rasterio.windows import Window def tile_image(src_path, tile_size=512, overlap=0.25): with rasterio.open(src_path) as src: height, width = src.height, src.width stride = int(tile_size * (1 - overlap)) tiles = [] for row in range(0, height, stride): for col in range(0, width, stride): w = min(tile_size, width - col) h = min(tile_size, height - row) if w < tile_size or h < tile_size: continue window = Window(col, row, w, h) tile = src.read(window=window) tiles.append((tile, row, col)) return tiles3.2 标注数据的清洗与类别平衡
遥感标注数据最大的问题是噪声。不同标注员对同一块地物的判断可能不一致,比如稀疏草地到底算草地还是裸地,边界模糊的区域经常标错。我拿到标注后第一件事是做一致性检查:把同一区域的多份标注叠在一起,只保留完全一致的部分,有争议的要么重新确认,要么直接丢弃。这一步会损失大概15%到20%的标注量,但剩下的数据质量高很多,训练出来的模型稳定得多。
类别不平衡的处理也很关键。我的做法不是简单地上采样少数类,而是用类别加权损失函数。具体来说,每个类别的权重设为该类别像素总数的倒数,再归一化到均值为1。这样少数类的损失权重会大一些,但不会像重采样那样导致过拟合。另外,我还会在数据增强阶段对少数类做额外的旋转和翻转,增加其样本多样性。
提示:遥感影像的旋转增强要小心。自然图像旋转90度没问题,但遥感影像有地理方向性,比如农田的垄沟方向、建筑的朝向,过度旋转可能引入不真实的样本。我一般只做90度、180度、270度旋转和水平垂直翻转,不做任意角度旋转。
3.3 归一化与波段处理
高分辨率遥感影像通常有四个波段:红、绿、蓝、近红外。预训练网络是在RGB三通道上训练的,所以要么把近红外丢掉,要么把它映射到某个通道。我试过两种方案:一是直接用RGB三通道,简单但丢了近红外信息;二是把近红外当成第四个通道,复制预训练权重的第一层卷积核,多出来的通道用零初始化或者均值初始化。实测下来,第二种方案在植被分类上精度高不少,因为近红外对植被健康状态非常敏感。
归一化方面,不能直接用ImageNet的均值和方差,因为遥感影像的像素分布和自然图像差别很大。我的做法是统计自己数据集的均值和方差,然后做标准化。如果数据集不大,可以用整个训练集的全局统计量;如果数据集很大,可以分区域统计,避免不同区域的光照差异影响。
4. 模型搭建与训练实操
4.1 网络结构改造
ResNet-50原本是用于图像分类的,输出是一个1000维的向量。要做像素级分类,需要把最后的全连接层去掉,换成卷积层和上采样层。我的做法是保留ResNet-50的前面所有卷积层,在layer4后面接一个1x1卷积把通道数降到类别数,然后用双线性插值上采样回原图尺寸。这样输出的就是每个像素的类别概率图。
具体结构是这样的:输入512x512x4(如果用了近红外),经过ResNet-50的卷积层后,特征图变成16x16x2048,然后1x1卷积降到16x16xN(N是类别数),再经过三次上采样(每次放大2倍)变成128x128xN,最后再上采样一次到512x512xN。上采样用双线性插值而不是转置卷积,因为转置卷积容易产生棋盘格伪影,而遥感影像对伪影很敏感,会干扰后续的面积统计。
import torch import torch.nn as nn import torchvision.models as models class RemoteSensingSegmenter(nn.Module): def __init__(self, num_classes, in_channels=4): super().__init__() resnet = models.resnet50(pretrained=True) # 修改第一层以接受4通道输入 if in_channels == 4: old_conv = resnet.conv1 new_conv = nn.Conv2d(4, 64, kernel_size=7, stride=2, padding=3, bias=False) new_conv.weight.data[:, :3] = old_conv.weight.data new_conv.weight.data[:, 3] = old_conv.weight.data.mean(dim=1) resnet.conv1 = new_conv self.backbone = nn.Sequential(*list(resnet.children())[:-2]) self.head = nn.Conv2d(2048, num_classes, kernel_size=1) self.upsample = nn.Upsample(scale_factor=32, mode='bilinear', align_corners=False) def forward(self, x): features = self.backbone(x) out = self.head(features) return self.upsample(out)4.2 损失函数与优化器配置
损失函数我用的是加权交叉熵加Dice损失的组合。交叉熵负责像素级分类,Dice损失负责类别间的区域重叠度,两者结合能同时优化像素精度和区域完整性。权重比例我设的是交叉熵占0.7,Dice占0.3,这个比例是试出来的,交叉熵太高会导致小类别被淹没,Dice太高会让边界变得模糊。
优化器用AdamW,学习率按前面说的分层设置。权重衰减设0.01,这是AdamW的推荐值。批次大小根据显存来,单卡11GB的话,512x512的图块可以放8个。如果显存不够,可以降到256x256的图块,批次大小翻倍,但要注意小图块的上下文信息少,精度会略降。
训练轮数我一般设50到80轮,配合余弦退火学习率调度。前5轮做warmup,学习率从1e-5线性升到设定值,避免一开始就大学习率破坏预训练权重。之后每10轮验证一次,如果验证集精度连续3轮不提升就提前停止。
4.3 训练过程中的监控指标
光看损失值不够,遥感分类要看几个关键指标:总体精度、Kappa系数、各类别的F1分数。总体精度就是所有像素里分对的比例,但它在类别不平衡时会虚高,所以必须看Kappa系数,它排除了随机一致性的影响。F1分数是精确率和召回率的调和平均,能反映每个类别的分类质量。
我一般每轮训练后在验证集上算这三个指标,记录到TensorBoard里。如果发现某个类别的F1一直上不去,就要检查是不是样本太少或者标注有问题。有一次我发现水体的F1只有0.6,其他类别都在0.85以上,排查后发现是浅水区和深水区的标注混在一起了,模型学不到统一的特征。后来把水体细分成深水和浅水两个子类,F1就上到0.82了。
5. 迁移效果评估与调优经验
5.1 迁移前后的精度对比
为了验证迁移学习的价值,我做过一组对照实验:一组从零训练,一组用预训练权重迁移。其他条件完全一样,结果差异非常明显。
| 训练方式 | 总体精度 | Kappa系数 | 训练轮数 | 收敛时间 |
|---|---|---|---|---|
| 从零训练 | 0.72 | 0.61 | 120 | 约18小时 |
| 迁移学习(冻结底层) | 0.86 | 0.79 | 45 | 约6小时 |
| 迁移学习(全网络微调) | 0.84 | 0.76 | 60 | 约9小时 |
从零训练不仅精度低,而且训练轮数多、收敛慢,验证集精度波动也大。迁移学习冻结底层的方案精度最高,因为底层通用特征被完整保留,高层特征又能充分适配遥感类别。全网络微调反而略低,原因是底层特征被遥感数据“带偏”了,丢失了通用性。
5.2 不同迁移策略的消融实验
除了冻结和全微调,我还试过几种中间策略,这里把结果整理出来,方便你根据自己情况选择。
| 策略 | 冻结层 | 可训练层 | 总体精度 | 备注 |
|---|---|---|---|---|
| 策略A | conv1-layer2 | layer3-layer4+head | 0.86 | 推荐,平衡精度和速度 |
| 策略B | conv1-layer3 | layer4+head | 0.83 | 训练快,但精度略低 |
| 策略C | 无 | 全部 | 0.84 | 需要更多数据,否则过拟合 |
| 策略D | conv1-layer4 | head only | 0.78 | 只适合类别极少的场景 |
策略A是我最常用的,它在精度和训练成本之间取得了很好的平衡。策略B适合快速验证想法,比如你刚拿到数据想看看大概能分几类。策略C需要至少五万级像素样本,否则验证集精度会明显低于训练集。策略D基本只适合二分类任务,多类别时高层特征不够用。
5.3 跨区域迁移的泛化表现
迁移学习还有一个隐藏价值:跨区域泛化。我在A区域训练的模型,直接拿到B区域推理,如果不做任何适配,精度会掉10到15个百分点。但如果用B区域的少量样本做微调,哪怕只有几百个像素,精度就能恢复到接近A区域的水平。这说明预训练权重提供的通用特征确实有很强的跨域能力,只需要少量目标域数据就能完成适配。
具体操作上,我会先在A区域训练好模型,然后把B区域的少量标注样本拿出来,冻结底层,只微调layer4和分类头,学习率设1e-4,训练10到15轮就够了。这个过程通常只需要几十分钟,比从零训练快两个数量级。
注意:跨区域迁移时,如果两个区域的光照、季节差异很大,最好先做色彩归一化。我试过用直方图匹配把B区域的色彩分布对齐到A区域,精度能再提升3到5个百分点。这个方法在遥感里很实用,因为不同季节的植被颜色差异极大。
6. 常见问题与排查技巧实录
6.1 训练不收敛或精度震荡
这是最常见的问题,原因通常有三个:学习率太大、批次太小、数据标注噪声太多。排查顺序是:先把学习率降一个数量级,如果还震荡,检查批次大小是否小于4,如果小于4就增大图块尺寸或者用梯度累积。如果这两步都没用,那大概率是标注问题,需要抽样检查标注质量。
我遇到过一次精度剧烈震荡,最后发现是数据增强里的随机旋转用了任意角度,导致有些图块旋转后地物方向完全失真。改成只做90度倍数旋转后,震荡就消失了。遥感影像的方向性比自然图像强得多,增强策略要保守一些。
6.2 小类别分类效果差
小类别F1低,首先要看样本量。如果某个类别的像素数不到总像素的1%,那模型很难学到有效特征。解决办法有两个:一是增加该类别样本,可以通过旋转、翻转、加噪声等方式扩充;二是调整损失权重,把该类别权重调高。我一般会把权重设为该类别像素占比倒数的平方根,这样既不会过度放大,又能给小类别足够的关注。
如果样本量够了但F1还是低,那可能是类别定义有问题。比如“裸地”和“稀疏草地”在光谱上很接近,模型很难区分。这时候要么合并类别,要么引入额外的辅助信息,比如纹理特征或者时序信息。我试过在输入里加一个NDVI指数通道,对区分植被和裸地帮助很大。
6.3 推理结果有拼接缝
拼接缝是分块推理的典型问题。前面说了用25%重叠可以缓解,但如果重叠区域的平均策略没做好,还是会有痕迹。我的做法是在重叠区域用高斯权重做加权平均,中心像素权重高,边缘像素权重低。这样拼接缝基本看不出来。
还有一个细节:分块推理时,每个图块都要做和训练时一样的归一化。如果训练时用的是全局均值和方差,推理时也要用同一套参数,不能重新统计。我见过有人推理时对每个图块单独做归一化,结果不同图块的色彩不一致,拼接后像打了补丁。
6.4 显存不足的应对方案
高分辨率影像加深度网络,显存很容易爆。除了减小图块尺寸和批次大小,还有几个技巧:一是用混合精度训练,能省30%到40%的显存;二是用梯度检查点,牺牲一点速度换显存;三是把模型切成两半,分别放在两张卡上做模型并行。我一般优先用混合精度,因为改动最小,效果也最明显。
from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() for images, labels in dataloader: optimizer.zero_grad() with autocast(): outputs = model(images) loss = criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()6.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 损失不下降 | 学习率过大/过小 | 打印梯度范数 | 调整学习率,加warmup |
| 验证精度远低于训练 | 过拟合 | 对比训练验证曲线 | 加正则化,增数据增强 |
| 小类别F1低 | 样本不足/权重低 | 统计类别分布 | 调权重,扩充样本 |
| 拼接缝明显 | 重叠不足/归一化不一致 | 检查重叠率和归一化参数 | 增重叠,统一归一化 |
| 显存溢出 | 批次太大/图块太大 | 监控显存占用 | 混合精度,梯度检查点 |
| 跨区域精度骤降 | 域差异大 | 对比两区域色彩分布 | 色彩归一化,少量微调 |
7. 实操心得与后续扩展方向
这个项目做下来,我最大的体会是:迁移学习的核心不是模型有多深,而是迁移策略有多细。同样的ResNet-50,分层学习率设得好,精度能比全局统一学习率高3到5个百分点。很多人把预训练权重加载进来就完事了,其实那只是第一步,后面的分层微调、损失加权、数据增强策略才是决定成败的关键。
另外,遥感影像的预处理比自然图像重要得多。自然图像你随便裁剪、旋转、缩放,语义基本不变。但遥感影像有地理坐标、有方向性、有季节差异,预处理不当会引入大量噪声。我建议在做任何模型训练之前,先花时间把数据清洗和增强策略定好,磨刀不误砍柴工。
后续如果想进一步提升,有几个方向可以尝试。一是引入多时相数据,同一区域不同季节的影像能提供额外的判别信息,比如农田在播种期和收割期的光谱差异很大,多时相融合能显著提升分类精度。二是用自监督预训练,在大量无标注遥感影像上先做对比学习,再迁移到有标注任务上,这样能进一步减少对标注的依赖。三是尝试轻量化模型,把ResNet-50换成MobileNetV3或者EfficientNet-Lite,在保持精度的同时把推理速度提上去,方便部署到边缘设备上做实时分类。
最后分享一个小技巧:如果你手头的标注数据实在少,可以先用预训练模型做一遍推理,把高置信度的预测结果当成伪标签,和真实标注一起训练。这个方法在遥感里叫自训练,我试过用10%的标注加90%的伪标签,精度能达到全标注的85%左右,性价比很高。当然伪标签的阈值要调好,太高了样本不够,太低了噪声太多,一般设0.9左右比较稳。