多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

卫星云图识别与理解:基于PyTorch的迁移学习与消融实验实战

卫星云图识别与理解:基于PyTorch的迁移学习与消融实验实战 简介图像识别与语义分割是计算机视觉的两大核心任务分别回答“图像是什么”与“每个像素属于什么”的问题。在实际工程中这类技术被广泛应用于气象遥感、灾害监测等领域。以卫星云图为例其数据常存在单通道、16bit位深、类别不平衡等特性让通用图像分类流程直接套用时频频出错。本文从数据预处理入手介绍如何正确读取与增强云图数据并基于PyTorch构建基线CNN模型进一步通过迁移学习ResNet18微调与消融实验提升精度同时给出宏F1与IoU等更可靠的评价指标。最终形成一套从数据盘查、模型训练到结果交付的完整实践方案适用于课程大作业与小型遥感项目。1. 卫星云层图像的理解与识别这份大作业到底在考什么很多人拿到“卫星云层图像的理解与识别”这个计算机视觉大作业题目时第一反应是找现成源码改个loss结果从下载数据开始就卡住云图有RGB也有灰度有的还是16bit读出来整张图是黑的。“理解与识别”四个字没有告诉你任务边界这是它和普通图像分类最大的不同。把它拆成“云系全局分类 云区像素级识别”两个链路用PyTorch把数据预处理、基线CNN、迁移学习和消融实验串起来大多数样本量在几千到几万张的课程项目都能在一周内拿到稳定可交付的结果。这份笔记就按这套方案完整走一遍换到你自己的数据集上也能直接套。2. 任务定成分类还是理解先把云图和数据集“盘”清楚“理解与识别”在题目里是并列关系不是同一个任务。识别偏向全局判断——这整张云图是积雨云还是卷云理解偏向局部推断——云层覆盖了图像的哪片区域、哪里有浓云。两者可以分开做也可以共用一套数据管线。起步阶段先别急着写模型把数据盘明白任务边界自然就清楚了。2.1 拿到的数据里藏着哪些信息尺寸、通道与位深卫星云图和你平时用的自然图像在读取方式上有本质区别。自然图像基本是8bit三通道OpenCV或PIL直接imread就能看云图可能是单通道灰度、三通道合成、多通道遥感产品甚至带温度信息的16bit数据。第一步永远是用脚本统一检查这批数据的长宽、通道数和数值范围而不是先跑模型。import os import cv2 import numpy as np from collections import Counter img_dir cloud_data/train stats Counter() for fname in os.listdir(img_dir): if not fname.lower().endswith((.png, .jpg, .jpeg, .tif, .npy)): continue fpath os.path.join(img_dir, fname) img cv2.imread(fpath, cv2.IMREAD_UNCHANGED) stats[shape] [(img.shape)] stats[dtype] [str(img.dtype)] stats[min] [int(img.min())] stats[max] [int(img.max())] stats[unique_chn] [1 if img.ndim 2 else img.shape[2]] print(最常见的尺寸:, Counter(stats[shape]).most_common(3)) print(数据类型:, Counter(stats[dtype]).most_common()) print(通道数分布:, Counter(stats[unique_chn]).most_common()) print(灰度范围示例:, Counter(stats[min]).most_common(3))这段脚本用cv2.IMREAD_UNCHANGED按原始位深读图避开默认转8bit导致的全黑或全白问题。把 shape、dtype、数值范围统计出来你会直观看到数据是否统一——比如有没有混入不同尺寸的图、有没有部分是单通道部分是三通道。之后所有预处理都以这份统计为基准而不是赌每张图能读出来。如果你的数据里混着.npy或.npz格式用np.load单独处理不要侥幸交给cv2.imread。如果是老师提供的tar包或接口下载的多源数据先解压后跑一遍这个统计脚本通常是第一天最值钱的动作。2.2 识别、理解还是分割三个任务模型的难度与上限在确定网络结构之前先判断这个题目是让你做哪一种任务。最简单的是全局分类输出一个标签比如“积雨云/层云/卷云/晴空”中等难度是目标检测或区域分类判断“图的左上角有一片积雨云”最完整的是语义分割输出和原图同尺寸的掩膜每个像素判断是云还是非云。三者差距不只是模型复杂度更是标注成本。分类任务只要有几百到几千张带标签的整图就能起步分割任务需要逐像素标注很多课程作业提供的是低分辨率掩膜或者干脆不提供需要你自己想办法。常见做法是分类为主任务把“理解”拆成对分类结果的置信度可视化再把云区占比作为辅助信息输出。这样既回应了“理解”二字又不至于陷进大规模人工标注。模型选型也随任务走。分类用ResNet18或更浅的CNN足够分割至少得是UNet或轻量DeepLab还得配一个像样的预训练主干。综合交付周期考虑我的建议是先用分类把流程跑通拿到基线分再评估要不要加一版分割作为加分项。很多大作业的评分点其实落在“实验完整度”上而不是单一测试集准确率。2.3 评价指标怎么选别只报一个准确率云图数据几乎天然类别不平衡晴空图远多于强对流云图有云的像素远多于无云的像素。这种情况下 accuracy 会骗人全猜多数类的模型也有很高的准确率。因此至少要同时报三个指标图表里才站得住脚。最核心的是宏平均F1macro F1它对每个类别单独算F1再取平均小类别不占便宜其次要给出混淆矩阵因为云系类型之间容易混淆比如层云和雾、卷云和高层云混淆矩阵能直接看出你的模型在哪些类别上摆烂如果做了分割还必须报IoU交并比因为分割任务里IoU比Dice更严格也更常被评审老师问到。写实验报告时把指标表做成三行基线样本类别比例一行、模型整体指标一行、每个类别的precision/recall/F1各一行。这样老师扫一眼就知道你关注了类别不平衡问题而不是只会看准确率。新人最容易犯的错就是只贴一个测试集准确率被追问“你有没有考虑样本不均衡”时答不上来分数直接掉档。3. 用 Python 把云图识别管线跑通读图、增强与基线模型任务定成分类之后接下来要做一条能跑通的最小管线。这里有一个很多人忽略的点先在少量数据上把模型训起来、保存、再推理比一开始就追求高精度更重要。管线不完整后面调参调得再好也没法交付。环境配置方面Python 3.8以上的发行版加一个能用的IDE基本够用PyTorch按官方命令装CPU或CUDA版不需要额外折腾验证环境时先跑一段import torch; print(torch.__version__)能过就说明环境没问题。3.1 从 OpenCV 到 Tensor按通道和位深正确读入云图云图预处理是翻车重灾区。先把统一的读图函数封装好后面所有代码都走这一个入口而不是每次各写各的。封装时要处理三个问题位深转换、灰度转三通道、尺寸统一。import cv2 import numpy as np import torch def load_cloud_image(path, target_size(224, 224), as_tensorTrue): img cv2.imread(path, cv2.IMREAD_UNCHANGED) if img is None: raise ValueError(f无法读取图片: {path}) # 16bit或更高位深转8bit先截断再缩放避免直接除255丢数据 if img.dtype np.uint16: img np.clip(img.astype(np.float32) / 257.0, 0, 255).astype(np.uint8) elif img.dtype ! np.uint8: img cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) if img.ndim 2: img cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) img cv2.resize(img, target_size, interpolationcv2.INTER_LINEAR) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img img.astype(np.float32) / 255.0 if as_tensor: # 转为 [C, H, W] 的张量 return torch.from_numpy(img).permute(2, 0, 1) return img说明三点。第一16bit图除以257而不是255因为16bit满值是65535257正好是255倍这样能把0到65535映射到0到255这一处很多教程都写错导致图像整体偏暗或信息丢失。第二灰度图复制成三通道是为了后面能直接喂给ResNet或VGG这种要求三通道输入的预训练模型。第三统一resize到224x224这是ImageNet标准输入尺寸也是后续用迁移学习时最省事的做法。如果你在Windows上装了多个Python环境cv2.imread读不过去多半不是环境问题而是路径里有中文换英文路径即可。这点在云图项目里特别常见因为很多课程包默认路径就是“实验数据-最终版”。3.2 数据增强云图能用哪些增强不能用哪些数据增强不是无脑堆。云图和自然图像差异很大水平翻转和垂直翻转对云层语义是安全的积雨云翻过来还是积雨云但旋转90度要小心因为风的走向和云系的拉伸方向通常有物理含义旋转90度可能改变云的纹理朝向让模型学到错误的先验。另外云图对亮度敏感过强的颜色抖动会把卷云和晴空混在一起。一个稳妥的增强组合是随机水平翻转、随机垂直翻转、小幅度的亮度对比度扰动、随机裁剪再resize回原尺寸。这个组合基本不会破坏云图语义又能给你1到2个点的提升。把增强逻辑封装在训练集的Dataset里验证集和测试集不做随机增强只做保底归一化。import torchvision.transforms as T import random def get_train_transforms(size224): return T.Compose([ T.RandomHorizontalFlip(p0.5), T.RandomVerticalFlip(p0.5), T.ColorJitter(brightness0.2, contrast0.2), T.RandomResizedCrop(size, scale(0.8, 1.0)), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])这里RandomResizedCrop的 scale 从0.8起步不做很激进的裁剪因为云系通常是大尺度结构裁得太碎会把局部纹理误当成类别特征。Normalize用的是ImageNet的mean和std迁移学习时保持一致能让预训练权重发挥正常。如果你的数据是单通道灰度复制成三通道这套变换依然适用无需额外调整。参数上brightness和contrast的抖动幅度我一般控制在0.2以内超过0.3就要观察验证集是否出现异常波动。如果发现增强后训练集loss降到接近0、验证集反而掉点优先怀疑增强过猛再做减法。3.3 第一版基线模型一个小型 CNN 把流程跑通迁移学习之前建议先训练一个自己写的三层CNN。它收敛快、训练一个epoch只要几十秒能快速验证数据管线、样本标签和评估代码是否有问题。调大模型之前先确定“管线不出错”是省时间的核心习惯。import torch.nn as nn class TinyCloudNet(nn.Module): def __init__(self, num_classes4): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.AdaptiveAvgPool2d((1, 1)) ) self.classifier nn.Linear(64, num_classes) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) return self.classifier(x)这个网络结构上没什么新意但作为基线足够三层卷积加两个池化参数少、不容易过拟合到某个批次训练两三分钟就能看出收敛趋势。先跑通整个流程确认loss能稳定下降、验证集准确率在随机猜测之上再考虑换大模型。配套的训练循环里要注意三点。损失函数用nn.CrossEntropyLoss云图分类是标准多分类问题不需要额外定制优化器用SGD带momentum 0.9、初始学习率1e-3或者换成AdamW也行但AdamW的权重衰减我一般设0.01每个epoch结束在验证集上跑一次保存验证集指标最好那个checkpoint而不是最后一个epoch的权重。这个“保存最优checkpoint”的习惯能救回很多次训练后期过拟合的翻车。基线跑通后记录三件事作为后续对比的起点训练集准确率、验证集准确率、宏F1。之后所有改动都拿这三项对照有提升就留没有就回滚。4. 把识别精度从及格拉到优良调参、迁移学习与消融实验基线有了接下来就是在这个基础上做增量改进。这个阶段最容易陷入“乱调一气、不知道谁贡献了提升”的状态。我的建议是每一步改动都作为单独实验记录先把训练参数固定住再上迁移学习最后把实验整理成消融表报告和答辩都有底气。4.1 训练参数怎么调学习率、batch、epoch 与随机种子调整参数是有顺序的不要同时动多个变量。先固定batch size为32或64跑几个epoch观察loss下降曲线如果loss震荡厉害把学习率降到1e-4或3e-4如果loss在同一个水平反复横跳可能要加大batch size而不是加学习率。云图数据通常样本几千到几万batch size 32在多数显卡上都能跑CPU训练也勉强能接受。随机种子是复现的基本前提。不设seed两次训练结果天差地别没法判断是改动带来的提升还是运气。我遇到过一个典型情况加了数据增强后准确率提高2个点换了个随机种子直接掉回原样也是用seed固定问题排查出来的。import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False这段代码放在训练脚本最前面训练前调用一次。注意cudnn.deterministic True会让某些网络模块变慢但对复现结果来说值得。正式实验时不要今天seed 42明天seed 2024全程统一才能横向比较。学习率调度建议用余弦退火总epoch设25到40之间。云图数据集不大训练太多个epoch会过拟合loss看着很低验证集却在掉。配合早停机制连续5个epoch验证集指标没变好就停止能省下大量测试时间。4.2 迁移学习用 ImageNet 预训练模型微调是否靠谱卫星云图和ImageNet的自然图像差距不小但预训练模型的浅层特征——边缘、纹理、颜色过渡——依然有用。用ResNet18或ResNet34做微调是课程大作业性价比最高的操作比白手起家训练VGG快得多效果也稳。import torchvision.models as models import torch.nn as nn def build_finetune_model(num_classes4, freeze_backboneTrue): model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, num_classes) if freeze_backbone: for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True return model微调策略有两种。第一种是freeze_backboneTrue只训练最后的全连接层适合总体样本量少于几千的情况收敛快且不容易过拟合。第二种是整个模型解冻用较小学习率1e-4训练全部参数适合样本量上万的情况。两者折中的办法是解冻最后两个残差块前几层继续冻结这在实际项目中对比都兼顾。注意一个细节weightsmodels.ResNet18_Weights.IMAGENET1K_V1这种写法在较新的torchvision版本中会提示旧接口已弃用不一定报错但会警告。如果联网环境不允许下载预训练权重就不要强行迁移用3.3节的TinyCloudNet多跑几个epoch也能交差。4.3 消融实验用一张表说清每个改动贡献了多少准确率消融实验是让评分老师相信你的模型不是“黑匣子调参”的关键。把改动拆成阶梯式的几个实验每个实验只加一个变量记录指标变化。典型实验组设置如下。实验编号模型与设置验证集准确率宏F1ATinyCloudNet基线74.2%0.68BA 数据增强78.6%0.73CResNet18微调冻结主干83.1%0.78DC 解冻全部参数86.4%0.81ED 类别加权损失87.2%0.83这张表的价值在于每一步改动都能说清楚增强提升了4.4个点迁移学习提升了4.5个点解冻微调提升了3.3个点加权损失提升0.8个点但主要反映在小类别的F1上。老师问“为什么用ResNet18”时你可以直接指着表说“因为C比B高了4.5个点”比任何解释都有说服力。类加权损失的实现很简单在损失函数里把每个类的权重传进去。权重可以按样本数的倒数归一化让少数类的梯度贡献更大。class_weights torch.tensor([1.0, 1.2, 2.5, 3.0], devicedevice) criterion nn.CrossEntropyLoss(weightclass_weights)这里的权重数值不要拍脑袋先统计每类样本数n_i用max(n) / n_i做初值再微调。观测到小类别的F1上升、多数类F1轻微下降是正常现象这正是你要的平衡效果。如果小类别F1反而下降检查是不是权重设得过大导致训练不稳定。5. 卫星云图训练的 5 个高频踩坑点与排查办法这一章整理的是我在实际跑云图项目时反复遇到的坑每条都有明确的排查路径。写在这不只是为了让你避开也方便你定位问题时按图索骥省去一个个搜索的功夫。5.1 读出来全是黑图或白图位深和通道的问题现象用cv2.imread(path)读云图imshow出来不是一片黑就是一片白人眼完全看不出云层结构。原因这类云图往往是16bit无符号整型值域0到65535而cv2.imread默认按8bit读超出范围的数值直接被截断看起来就是纯色。另一种情况是二通道或四通道遥感产品被当成三通道处理通道信息错乱导致显示异常。解决读图时一律加cv2.IMREAD_UNCHANGED先打印dtype和min/max确认数值范围。16bit的图用压缩映射np.clip(img / 257.0, 0, 255).astype(np.uint8)不要直接除以255。如果数值范围极小比如只有100到500用cv2.normalize做min-max拉伸云层对比度会清晰很多。5.2 训练损失一直掉验证集指标却上窜下跳现象训练集准确率一路冲到90%以上验证集准确率在60%到75%之间大幅波动保存的最优checkpoint也救不回来。原因最典型的两种情况一是学习率过大模型在每个epoch都在优化目标附近振荡、无法收敛到稳定区域二是batch size太小导致每个batch的梯度方向方差太大loss和精度都跟着剧烈抖动。它不是过拟合过拟合是验证集缓慢下降不是来回跳。解决把学习率降到原来的十分之一也就是从1e-3降到1e-4观察两三个epoch。同时把batch size从16提到32或64让每个batch更接近整体分布。如果波动仍明显检查数据加载是否真的做了shuffle按类别顺序加载的数据会让每个batch的类别分布严重偏斜。5.3 数据增强加了不如不加物理语义违背现象训练集加上随机旋转、随机裁剪、颜色抖动后验证集准确率不升反降有时比基线还低两三个点。原因云图不是普通照片。旋转90度或180度会让云的阴影方向、风场走向等物理特征失真模型学的不是云本身的形态而是增强后的伪影。大幅度的裁剪则把云的上下文信息切掉了单一云块很难判断是积雨云还是层云。解决删除随机旋转和强裁剪只保留水平翻转、垂直翻转、幅值较小的亮度对比度抖动。具体参数上亮度幅度不超过0.2对比度不超过0.2随机裁剪的scale不低于0.8。另外在实验报告里写清楚“为什么不用旋转增强”评委老师会认为你真的理解云图数据。5.4 准确率很高但每个类的F1相差很大现象宏F1比准确率低了10个点以上准确率接近85%但某几个类别的recall只有30%左右混淆矩阵显示少数类大量被预测成多数类。原因样本类别不平衡时交叉熵损失被多数类主导。模型把所有样本都预测成多数类就能把loss压得很低准确率看起来不错小类别的learning则完全失败。解决先统计各类别样本数按max_n / n_i计算权重传给CrossEntropyLoss(weight...)。如果加了权重还是不行就做重采样用WeightedRandomSampler让每个epoch里少数类样本的出现次数增加这是更难翻车的手段。报告指标以宏F1为主不要用准确率掩盖不平衡问题。5.5 换台机器复现结果对不上现象在自己电脑上训练到87%准确率把源码交给同学在另一台机器上跑结果只有70%甚至loss曲线都不一样。原因随机种子没有固定只是其中一环更大的坑是两台机器的torch版本、CUDA版本、卷积算子实现不同同一套权重forward的结果都有微小差异。某些同学的机器上自动下载了不同版本的预训练权重坑就更深了。解决训练脚本里写死set_seed并打印当前环境信息torch版本、设备名推理时加载权重用torch.load(path, map_locationcpu)避免CUDA设备串号问题。如果对方机器上没有预训练权重缓存要么把权重文件随源码一起交付要么在README里写明下载地址和放置路径。复现不了不是玄学是环境没锁住。6. 从源码到交付让老师一眼看懂你做了什么大作业的评分往往先看源码结构和报告最后才是实验效果。一个结构混乱、README缺失、代码全是断点的项目即使准确率很高也会被要求重答。反过来清晰的项目组织能弥补实验上的小瑕疵。我第一次做这类大作业时把训练、验证、推理全塞在一个脚本里交了源码后被老师在生产环境里跑出了完全不同的结果最后通过补了checkpoint和固定seed才挽回了局面。所以目录组织是一个值得较真的习惯训练、推理、数据工具、文档各归其位谁拿到都能快速定位。目录/文件作用train.py训练入口包含数据加载、模型构建、训练循环、checkpoint保存inference.py推理脚本读入单张图或文件夹输出预测类别和置信度utils/data_loader.py读图函数、Dataset定义、数据增强封装utils/metrics.pyaccuracy、宏F1、混淆矩阵计算checkpoints/best_model.pth最优权重文件README.md环境要求、运行步骤、数据集说明docs/实验报告.md实验设计、消融表、结果分析README不要写套话直接写清三件事第一用哪个Python版本和依赖库列表最好附requirements.txt第二数据集放到哪个目录、每类文件夹怎么组织第三训练和推理各跑哪一行命令参数怎么改。演示时老师大概率按README顺序操作README走不通观感损失很大。报告的写法上建议用“结论先行”的结构。第一页概述表格直接给出消融实验的全部结果后面每章按“动机→方法→实验结果→失败案例”展开。PPT不要堆代码每页只放一个关键问题、一张示意图或一张数据表比如混淆矩阵热力图、训练loss曲线、正确和错误的预测各两张每张图下面配一句解释。按照CS231N这类课程作业汇报的惯例控制在一页一个结论讲清楚“我为什么这样设计”比“我的代码多长”更有价值。验证方法上我习惯把所有测试图的推理结果输出成一张拼接图左列为原图中列为真实标签右列为预测标签和置信度。这样一方面自己能直观看到模型在哪些图上犯困另一方面报告里截图也能直接反映泛化能力。推理脚本复用3.1节的读图函数保证训练和推理走同一个预处理入口。import torch import matplotlib.pyplot as plt # 复用 load_cloud_image 读图 img_tensor load_cloud_image(sample_path).unsqueeze(0) with torch.no_grad(): logits model(img_tensor) prob torch.softmax(logits, dim-1) pred_id torch.argmax(prob, dim-1).item() conf prob[0, pred_id].item() print(f预测类别: {pred_id}置信度: {conf:.3f})这段代码只做一件事加载一张图前向推理打印预测类别和置信度。注意unsqueeze(0)是因为模型输入是四维的[batch, C, H, W]单张图要先补一个batch维度。torch.softmax把logits转成概率再取最大值的下标作为类别。整个推理封装成函数后报告里直接引用就行。最后说一个习惯每次实验结束把模型参数、优化器状态、当前epoch数、最优指标打包进checkpoint而不是只存权重。这样无论是继续训练还是回溯对比都有后悔药可以吃。大作业的完整度往往比单点精度更能拉开差距源码、报告、PPT三件套都到位分数自然不会差。希望帮到你。本文还有配套的精品资源点击获取
返回列表