精华内容
下载资源
问答
  • 基于深度学习的遥感影像地块分割方法.pdf
  • 目录前言赛题介绍初赛阶段评测指标...遥感影像地块分割,旨在对遥感影像进行像素级内容分析,对遥感影像中感兴趣的类别进行提取和分类(说白了就是语义分割任务,只是对象是遥感图像)。 本次比赛分类的类别有七类,

    前言

    近期在实验室老师的推荐下开始了我的第一次比赛经历,由于课题与遥感影像相关,所以选择参加遥感影像地块分割(其实是数据集很有诱惑力嘻嘻),下面就这次比赛的过程和经验做个分享总结。

    赛题介绍

    遥感影像地块分割,旨在对遥感影像进行像素级内容分析,对遥感影像中感兴趣的类别进行提取和分类(说白了就是语义分割任务,只是对象是遥感图像)。
    任务示例图
    本次比赛分类的类别有七类,具体包括建筑、耕地、林地、水体、道路、草地和其他等七类。
    类别对应像素信息
    训练数据集:包含140,000张分辨率为2m/pixel,尺寸为256*256的JPG图片。
    A/B测试集:包含相同来源的1w/2w张测试图片。
    评测阶段是使用A测试集进行实时排名,最终成绩按B测试集成绩为准,这种评测形式设计特别不错(听说DataFountain平台的比赛都是这种形式)。
    由于这个赛题是百度的自主赛题,所以要求只能使用百度的深度学习框架PaddlePaddle,不能使用大家都熟知的TF和Pytorch框架。这次还使用了Paddle的分割套件PaddleSeg,着实感觉真香,所以在这里安利一下我们的国产AI框架(给百度打打广告嘻嘻)。

    初赛阶段

    评测指标

    初赛阶段的评价指标比较简单,使用语义分割的黄金指标,平均交并比miou(具体概念不多叙述)。
    平均交并比指标

    数据和难点分析

    从训练数据的类别占比上明显看出类别不均衡问题,如建筑、道路和草地这三类,所以如何改善类别不均衡问题是一大难点。

    类别标签占比 %
    建筑02.79
    耕地150.87
    林地217.87
    水体317.74
    道路40.35
    草地51.96
    其他67.38
    未标注区域2551.03

    而在前期训练和验证baseline模型结果发现,模型对道路和草地这两类的识别率都特别低,说明这两类对后续的结果影响最为大,我认为这也是最关键的难点(因为大家普遍都能训练好其他的类别,所以在这两类上才能拉开差距)。
    验证结果数据

    策略小结

    数据增强

    在预处理方面,对训练图像做归一化处理;在数据增强上,根据遥感图像的旋转不变性,使用了图像翻转缩放增强(训练数据足够大其实也不太容易过拟合)。
    图像翻转
    图像缩放

    模型训练

    损失函数:在损失函数的选择上,由于存在类别不均衡情况,尝试了Softmax loss、Weighted softmax loss 和Lovasz softmax loss(听说是kaggle常用神器)几种损失函数,最终选择Lovasz softmax loss和Softmax loss的混合损失函数。
    模型组合:在模型结构的选择上,也是尽可能地尝试几个较新的模型和骨干网络,最终选择DeepLab3+和ResNet50。我比较佛系,哪个跑得好就一直改那个,没去怼最新的模型(其实是没有足够GPU资源可以玩耍,每周靠白嫖AIStudio的70小时算力度过,相信这是大部分选手的痛点嘻嘻)。
    PaddleSeg模型介绍
    Attention机制:在模型改进上,尝试了几种即插即用的注意力机制,如Global Attention Upsample(GAU)、Convolutional Block Attention Module (CBAM)和SENet的Channel Attention Module,都有一点点提升,但不是很高,所以最终选择了简单实现的Channel Attention Module。
    Channel Attention Module

    弱类处理

    前面提到模型对道路和草地这两类的识别准确率低得感人,所以需要对这两类做额外的训练处理。
    道路类:首先在原训练集上提取一个道路二分类数据集,然后做单独的二分类,这里使用了U-Net+Dice loss组合训练,得到预测的道路二分类结果,最后通过简单的判断条件(两个标签上的道路类匹配度)覆盖到总预测结果上。
    草地类:对草地的单独训练则是提取耕地和草地两类出来做三分类(通过观察预测结果发现草地类易被错预测为耕地类),使用和主模型一样的网络训练,最后草地在总预测结果的覆盖上仅发生在草地和耕地两类之间。
    PS:虽然这两个类拎出来单独训练的准确率也不高(应该是自己菜的原因),但是还是能提一点分数的嘻嘻。

    预测策略

    单模型增强:单个模型的预测增强常用有多尺度预测和翻转预测,为控制预测时间,这里仅使用左右翻转和上下翻转各一次做预测增强(在预测前先做翻转,得到标签再翻转回来,这个应该容易理解吧)。单模型增强主要提升了预测的稳定性,所以不需要太多次。
    多模型增强:多模型的预测增强即模型集成方法,常用的模型集成方法包含bagging、boosting和stacking(这些我都不会emmmmm)。最终采用多模型预测结果硬投票的方式,少数服从多数嘛(我只会这个嘻嘻)。多模型增强主要靠模型之间的独立性,所以模型之间的相关性越低,集成得到效果更好(前提是单个模型不能太差,因为小学生带不动,反正我带不动)。

    初赛成绩

    从其他地方整理了许多提分的Trick,最后我能实现的也就上述这些简单的策略,最后初赛的成绩也还算可观哈(对于我这个竞赛小白已经挺满意啦,大佬们见笑了嘻嘻)。
    初赛成绩

    复赛阶段

    评测指标

    复赛阶段评测指标除了计算miou外,还结合遥感任务的特点,根据区域的连续性进行打分,这里仅针对水体类和道路类。
    复赛指标
    连通性指标计算
    上述计算公式中,我可以得出一个观点就是尽可能地连通邻近区域和减少无法连通处理的区域数(比如区域不大的噪点)。

    问题分析

    连通性指标仅针对水体类和道路类进行打分,所以需要解决两方面问题。
    准确性:首先需要保证这两类有较高的分类准确度,针对初赛阶段的结果,水体类的准确度足够高,但是道路类的准确度还特别低,需要提升道路类的识别准确性(未解决)。
    连通性:对连通性的考虑可以从两方面入手,一是从模型上入手,选择或改进模型,提高预测结果的连通完整性,二是后处理步骤改善连通性。

    连通性处理

    对提高连通性的考虑上,我第一时间想到的是使用图像形态学来处理这个问题。对水体和道路两类做相同处理,一是进行形态学闭操作(先膨胀后腐蚀,分别设置不同的膨胀腐蚀系数,获得连通的效果),二是去除小面积噪点。
    连通性处理结果
    如上图所示,图中蓝色为道路类,左边一列为未处理的图像,右边一列为形态学处理后的图像(找到两张还不错的图像展示,实际上道路的识别特别差劲呢)。
    上述的形态学操作在scikit-image包的morphology里面都有方便的API使用,这里用到的函数如下表。

    函数描述
    skimage.morphology.binary_closing(image,…)快速返回图像的二元形态闭合
    skimage.morphology.binary_dilation(image,…)快速返回图像的二进制形态膨胀
    skimage.morphology.binary_erosion(image,…)快速返回图像的二元形态学侵蚀
    skimage.morphology.remove_small_holes(ar,…)去除小于指定尺寸的连续孔
    skimage.morphology.remove_small_objects(ar)移除小于指定尺寸的连接组件

    复赛成绩

    虽然没能进入决赛且相比初赛名次有所退步,但在面对问题、分析问题和解决问题的过程还是有收获的。
    复赛成绩

    问题和经验总结

    (1)这次选择的主模型没有其他新模型的效果好,这可能决定了最终的上限。若有充足的时间和资源,建议尝试一些更大更新的模型结构,还有可以使用一些即插即用的模块作为提分利器。
    (2)本次比赛最大的问题是对道路类的识别太差劲,导致后续的工作无法进行下去。对于道路这类特殊类别,具有细且长等特点,可以针对性地选择合适的模型单练,还有不要随随便便更改模型结构(这次就是在魔改模型上花了很多时间),能改的别人基本都做过实验写进论文啦(多查查原论文)。
    (3)个人认为,其实对于连通性问题,使用形态学处理已经可以取得很好的效果了,前提是要有好的识别率。所以前面的问题应该早点去解决,不然会影响后续的问题处理,考虑问题要尽量全面。
    (4)要是你有队友的话,要做好合理的协商和分工,不要一个人埋头苦干。这次比赛也在网上看了许多比赛中trick的相关博客,大佬还是大佬,应该多向有经验的前辈请教嘻嘻。虽然我们都是抱着拿奖的目的参加比赛,但更要重视过程中的总结和收获哟!

    展开全文
  • 使用 VisualDL 助力遥感影像地块分割 (PaddleX 篇) 本项目利用 PaddleX 以及 PaddleSeg 两个套件分别实现遥感影像地块分割; 更重要的是想给大家展示一下如何利用VisualDL这个强大的可视化工具来辅助训练及调...
    • 本项目利用 PaddleX 以及 PaddleSeg 两个套件分别实现遥感影像地块分割;

    • 更重要的是想给大家展示一下如何利用VisualDL这个强大的可视化工具来辅助训练及调参;

    这个工具非常好用,在模型训练中给了我很大的帮助,也希望看完本文之后这个工具能给你带来帮助,也希望大家能去GitHub给点一点star,让官方把这个工具做的越来越好

    • 最近百度与CCF合作举办了遥感影像地块分割的比赛,希望该项目可以帮助大家提高成绩;

    • PaddleX 篇也即本篇的目的在于重点介绍 VisualDLScalar、Image 功能,以及如何利用它们来辅助我们的训练,其次会简单介绍一下 VisualDL 的其他功能;

    • PaddleSeg 篇的目的在于重点介绍VisualDLGraph、Histogram 功能,以及如何利用它们来帮助我们进行网络模型结构的设计;

    • 两篇都是通过遥感影像地块分割为应用背景,在实战中体验如何应用VisualDL

    背景及工具介绍

    • 遥感影像地块分割 旨在对遥感影像进行像素级内容解析,对遥感影像中感兴趣的类别进行提取和分类。也就是利用图像分割提取出图像中的房屋,水域,农田等用地类型,在城乡规划、防汛救灾等领域具有很高的实用价值
    • PaddleX 是飞桨的全流程开发工具,将深度学习开发全流程从数据准备、模型训练与优化到多端部署端到端打通,并提供统一任务API接口及图形化开发界面Demo。开发者无需分别安装不同套件,以低代码的形式即可快速完成飞桨全流程开发。总之就是用起来十分方便快捷,具体细节大家可以自行去 PaddleX Github 主页查看;
      在遥感方面,PaddleX 也提供了相应的demo,我们今天的项目基于PaddleX的 RGB遥感影像分割 来做;
    • VisualDL 是飞桨可视化分析工具,以丰富的图表呈现训练参数变化趋势、模型结构、数据样本、高维数据分布等。可帮助用户更清晰直观地理解深度学习模型训练过程及模型结构,进而实现高效的模型优化。支持标量、图结构、数据样本可视化、直方图、PR曲线及高维数据降维呈现等诸多功能,同时VisualDL提供可视化结果保存服务。具体细节大家可以自行去 VisualDL Github 主页查看;

    环境安装

    (1) 安装PaddleX

    !pip install paddlex -i https://mirror.baidu.com/pypi/simple
    

    (2) 安装VisualDL

    !python -m pip install visualdl --upgrade -i https://mirror.baidu.com/pypi/simple
    

    数据准备

    • 该 demo 使用2015 CCF大数据比赛提供的高清遥感影像,包含5张带标注的RGB图像,图像尺寸最大有7969 × 7939、最小有4011 × 2470。

    • 该数据集共标注了5类物体,分别是背景(标记为0)、植被(标记为1)、建筑(标记为2)、水体(标记为3)、道路 (标记为4)。

    • 本案例将前4张图片划分入训练集,第5张图片作为验证集。

    • 为增加训练时的批量大小,以滑动窗口为(1024,1024)、步长为(512, 512)对前4张图片进行切分,加上原本的4张大尺寸图片,训练集一共有688张图片。

    • 在训练过程中直接对大图片进行验证会导致显存不足,为避免此类问题的出现,针对验证集以滑动窗口为(769, 769)、步长为(769,769)对第5张图片进行切分,得到40张子图片。

    (1)我们在 work 目录下新建一个PaddleX文件夹,用来存放该项目与PaddleX相关的文件;创建好之后切换到PaddleX目录下作为当前工作目录;

    %cd work/PaddleX/
    

    (2)通过执行以下脚本即可下载数据集并一键完成上述数据处理(需要等待一段时间,会自动下载数据集并进行处理, 很快啊,大概2分钟)

    我们下载的原始数据集压缩包为 ccf_remote_dataset.tar.gz,解压后的文件夹为 ccf_remote_dataset,处理后的数据集为 dataset

    !python ./prepare_data.py
    

    训练配置

    • 在训练开始之前,我们需要设置使用的 增强策略数据集,以及网络结构

    • 我们在PaddleX/visualize_transforms.py 中实现了这些设置,

    • 下面没有直接执行脚本生成日志文件,而是将脚本中的各个部分抽取出来讲解了一下,训练之后的部分会直接运行写好的脚本。

    (1)环境变量配置

    # 用于控制是否使用GPU
    # 说明文档:https://paddlex.readthedocs.io/zh_CN/develop/appendix/parameters.html#gpu
    import os
    os.environ['CUDA_VISIBLE_DEVICES'] = '0'
    import matplotlib
    matplotlib.use('Agg')
    

    (2)设置增强策略

    我们定义的数据增强操作包含RandomPaddingCropRandomHorizontalFlipRandomVerticalFlipNormalize等。

    更多的数据增强策略,大家可以去查看PaddleX的在线文档

    import paddlex as pdx
    from paddlex.seg import transforms
    
    # 定义训练和验证时的transforms
    # API说明 https://paddlex.readthedocs.io/zh_CN/develop/apis/transforms/seg_transforms.html
    train_transforms = transforms.Compose([
        transforms.RandomPaddingCrop(crop_size=769),
        transforms.RandomHorizontalFlip(prob=1), 
        transforms.RandomVerticalFlip(prob=1),
        transforms.RandomBlur(prob=1), #这里概率参数设置为 1 是为了查看效果, 如果决定使用时请设置合适的概率
        transforms.RandomRotate(rotate_range=35),
        transforms.RandomDistort(brightness_prob=1, contrast_prob=1, saturation_prob=1, hue_prob=1),
        transforms.Normalize()
    ])
    
    eval_transforms = transforms.Compose(
        [transforms.Padding(target_size=769), transforms.Normalize()])
    

    (3)设置数据集

    # 定义训练和验证所用的数据集
    # API说明:https://paddlex.readthedocs.io/zh_CN/develop/apis/datasets.html#paddlex-datasets-segdataset
    train_dataset = pdx.datasets.SegDataset(
        data_dir='dataset',
        file_list='dataset/train_list.txt',
        label_list='dataset/labels.txt',
        transforms=train_transforms,
        shuffle=True)
    eval_dataset = pdx.datasets.SegDataset(
        data_dir='dataset',
        file_list='dataset/val_list.txt',
        label_list='dataset/labels.txt',
        transforms=eval_transforms)
    

    通过VisualDL-Image查看数据增强过程

    • 在设置好增强策略及数据集之后,我们就可以使用 VisualDL-Image 功能对数据增强过程进行可视化,从而查看我们选择的增强策略是否达到了我们预期的效果。

    • 运行以下命令将这些可视化结果保存为VisualDL能够识别的日志文件:

    pdx.transforms.visualize(train_dataset, #数据集读取器
                             img_count=4, #需要进行数据预处理/增强的图像数目
                             save_dir='output/deeplabv3p_mobilenetv3_large_ssld/vdl_out') #日志保存的路径
    

    如果是在Aistudio上运行本项目,

    则点击左侧面板的可视化->设置logdir->添加->

    选择日志目录output/deeplabv3p_mobilenetv3_large_ssld/vdl_out/vdl_out/image_transforms/->

    启动VisualDL服务 -> 打开VisualDL

    打开VisualDL面板后,点击上方标签样本数据 -> 样本数据-图像即可。

    下面第一行是原图,第二行是做了 RandomPaddingCrop 的可视化结果:

    从可视化结果中看出,经过RandomPaddingCrop后,图像的局部信息被放大,

    我们crop出了一块 769*769 的区域,这样的好处是可以减小显存压力,不同于resize, resize 的时候会使图像形变,但坏处也很明显,就是丢失了一部分图像信息,被我们crop掉的图像信息都没有了。

    下面第一行是 RandomRotate 后的结果,第二行是 RandomDistort 后的结果:

    可以看出 RandomRotate 在我们设定的角度内进行旋转,有时候旋转的会很大,有时候几乎没有动;

    最后这个 RandomDistort 随机像素替换看起来效果就不怎么好了,尤其是图三,要用这种增强方式的话,最好使用VisualDL的可视化功能来辅助你调整参数,从而避免出现这种情况;

    更多的图片我就不展示了,通过VisualDL的这个功能是不是能对数据增强有了更深的理解呢?

    具体选用什么增强策略,还是要结合实际情况来进行选择,如果你希望模型能在某个特定数据集上取得很好的效果,那么就可以通过观察该数据集的特征,然后尽量让训练集通过数据增强来尽可能与该数据集特征保持一致;

    模型训练

    • 我们选择 DeepLabv3p 作为网络结构,在 train 函数的参数中设置 use_vdl=True 即可使用VisualDL的功能,日志会保存在结果保存路径 save_dir 下的 vdl_log 文件夹下

    • 这里的其它参数如 train_batch_sizelearning_rate 首先要在显存允许的范围内进行调整,并且要同步调整;

    • 如果报错 out of memory 显存溢出,我们就调小 batch_size , 并且同比例调小 learning_rate, 一般是以2的幂次倍调整的;

    • 其次,learning_rate 的调整是非常关键的,设置不同的值完全会带来不同的效果,这时候我们就可以利用 VisualDL-Scalar 来观察 loss 曲线, 得到一个最优的学习率来提升我们模型的效果;

    注意 : 你可以先不要运行此处的代码,先看看下面我是如何进行参数的调整的,之后再回来进行训练

    ## 初始化模型,并进行训练
    ## 可使用VisualDL查看训练指标,参考https://paddlex.readthedocs.io/zh_CN/develop/train/visualdl.html
    num_classes = len(train_dataset.labels)
    
    # API说明:https://paddlex.readthedocs.io/zh_CN/develop/apis/models/semantic_segmentation.html#paddlex-seg-deeplabv3p
    model = pdx.seg.DeepLabv3p(
        num_classes=num_classes,
        backbone='MobileNetV3_large_x1_0_ssld',
        pooling_crop_size=(769, 769))
    # API说明:https://paddlex.readthedocs.io/zh_CN/develop/apis/models/semantic_segmentation.html#train
    # 各参数介绍与调整说明:https://paddlex.readthedocs.io/zh_CN/develop/appendix/parameters.html
    model.train(
        num_epochs=300,
        train_dataset=train_dataset,
        train_batch_size=8,
        eval_dataset=eval_dataset,
        learning_rate=0.005,
        save_interval_epochs=30,
        pretrain_weights='CITYSCAPES',
        save_dir='output/deeplabv3p_mobilenetv3_large_ssld', #可视化结果保存在该目录的 vdl_log 文件夹下
        use_vdl=True) #使用内置的VisualDL
    

    通过VisualDL-Scalar查看训练过程

    • VisualDL-Scalar: 也就是启动服务后的标量数据,以图表形式实时展示训练过程参数,如loss、miou、accuracy。让用户通过观察单组或多组训练参数变化,了解训练过程,加速模型调优

    注意: 下面是进行调整的过程,我在实验时并未调整上面的参数:save_dir 你如果想进行多组实验的对比,可以通过修改save_dir 来实现,而不必像我后面介绍的那样手动移动 log 文件

    我们第一次选择num_epochs=5, train_batch_size=8, learning_rate=0.5,观察一下 loss 曲线;

    训练5个epoch大概5分钟,之后类似上面查看增强效果的操作,设置 logdir 为 work/PaddleX/output/deeplabv3p_mobilenetv3_large_ssld/vdl_log

    然后启动VisualDL服务,选择标量数据,我们就可以看到loss曲线了

    从上面的曲线中我们看到 loss 曲线在刚开始时有上扬的情况,这表明学习率过大,我们需要调小学习率并从头训练

    同时如果loss曲线频繁震荡,也是需要调小学习率的;

    第二次我们设置 num_epochs=5, train_batch_size=8, learning_rate=0.0001

    开始第二次训练时,你可能需要重启执行器来释放掉显存,重启执行器的操作:右上角 代码执行器 -> 重启执行器;

    重启之后别忘记切换到工作目录,并进行训练前的一系列配置;安装PaddleX 及 VisualDL的步骤是不需要重复进行的;

    5个epoch之后的loss曲线如下图所示:

    从上面的曲线中可以看到 在调小学习率之后,开始没有出现上扬的情况;

    但可以发现,曲线收敛变慢了,与上一次相比 5 个 epoch 后,loss 还大于 1.2,并且下降趋势不明显;

    这表明我们的学习率设置过小了,需要调大学习率并从头训练

    我们第三次调整 num_epochs=10, train_batch_size=8, learning_rate=0.005

    这次我们把三次的曲线放在一起对比一下,VisualDL-scalar 是支持多组实验对比的,

    我们把日志放在三个文件夹下,并设置这三个文件夹为 logdir, 启动服务就可以看到了;

    可以看到第三次的loss曲线已经比较好了,同时避免了前两次的缺点;勾选右侧仅显示平滑后数据,更清晰的做一下对比;

    其中 蓝色为第一次学习率过大,绿色为第二次学习率过小,紫色为第三次学习率适中;

    我们查看一下显存占用,发现大概15G左右,我们可以选择扩大 batch_size=16 ,同比例扩大 lr=0.01,大家可以自己体验一下;

    利用 VisualDL 我们在很短的时间内,就可以选择合适的增强策略,以及 lr 与 batch_size,让我们的模型有了一个好的开始,

    下面我们正常开始训练,然后利用 VisualDL 的其他功能查看一下训练中的模型情况,以及最后的预测结果;

    我设置 epoch_nums=300, save_interval_epochs=30,这样每过30个epoch就可以保存一个模型出来,

    在训练的过程中,我们就可以将 vdl_log 设置为 logdir 来实时查看训练时的指标变化了;

    虽然我设置了 一共训练300个epoch但是通过下图我们可以看到,在120个epoch之后,loss曲线与miou都已经趋于平缓了,所以我在epoch =181 时停止了训练;

    利用VisualDL-Scalar,我们可以发现模型在什么时候达到了比较好的效果,从而可以及时终止训练,节省出大量的时间

    利用VisualDL-Image 查看预测结果

    • 我们在训练时每30个epoch保存了一个模型,我们可以将这些模型的预测结果放在一起进行对比,也可以将单个模型的多张预测结果放在一起查看效果;

    • 利用该功能进行横向纵向的对比,我们可以很快的调出我们最满意的模型,或者能够很快发现模型的问题;

    • 下面提供的脚本接收 3 个参数:

    第一个参数 model_save_dir 是模型保存的路径在哪里,vdl_log 也在这个文件夹下;最后我们的结果也会保存在该目录下的 vis_predict_log/

    第二个参数 epoch_name 是想查看的模型轮次,比如你想查看 epoch_30、 epoch_60 两个模型的效果,就可以设置 epoch_name='30 60'

    第三个参数 pic_num 是每个模型想要查看的预测数量,我们会在 dataset/val_list.txt 随机取出这么多图片来进行预测,设置 pic_num=2 就会每个模型取两张图片来进行展示

    • 但是注意

    实际记录进去的图片,会按照某种均等算法进行采样,每个tag下,前端只会显示采样后的10张图片

    如果你查看的模型数量超过了十个,虽然所有图像数据都会存储在日志的,但是显示的时候会进行采样,并不能同时查看你想查看的这十个模型的效果

    你可以通过LogReader读出来当初你记录的所有图像数据;或者多次启动服务进行多次采样;或者每次仅保存十个模型的数据;都是很方便的!

    • 我选用了epoch_30 到 epoch_180 六个模型,每个模型选取了两张预测结果,执行下面的脚本后,启动服务进行查看:

    这个脚本仅用了两行代码就完成了 VisualDL-Image 的使用,

    vdl_writer = LogWriter(out_dir)在我们指定的路径创建一个记录器

    vdl_writer.add_image(tag=tag, img=img, step=0)把我们想查看的图片记录下来

    !python ./visualize_prediction.py  --model_save_dir './output/deeplabv3p_mobilenetv3_large_ssld' --epoch_name '30 60 90 120 150 180' --pic_num 2
    

    开启服务后,同样选择 样本数据 -> 样本数据-图像 进行查看,

    第一行展示的为原图,第二行展示的为预测结果,

    第二行图像可以选择不同的模型进行对比,其中step: 后面的数字就是你刚才输入的参数epoch_name

    通过点击拖动条的不同区域,就可以对比不同模型的效果了

    之后的这些功能我们只是在PaddleX 篇中简单体验一下,具体如何应用到我们的项目当中,请查看PaddleSeg 篇

    通过 VisualDL-Graph 查看模型网络结构

    • 这个功能用起来十分方便,

    • 在本地使用时,开启VisualDL 服务后,直接把模型拖拽到 网络结构 即可

    • 在AIStudio 上,从左测标签栏里依次点击:可视化 -> 选择模型文件,找到我们的模型文件

    work/PaddleX/output/deeplabv3p_mobilenetv3_large_ssld/best_model/model.pdmodel
    -> 启动VisualDL服务

    在启动服务后,点击网络结构,就能查看我们的模型结构了;

    注意 :在打开比较大的模型时,速度会比较慢,电脑性能较差时,可能会卡死页面,建议在本地体验该功能;

    下面是此次训练的模型结构图部分展示:

    通过VisualDL-Histogram查看参数分布直方图

    • 我们可以通过上面网络结构查看一些参数的名称,在下面脚本中替换 vis_var_names进行查看;这里选取了[‘conv10_expand_weights’, ‘conv11_expand_weights’, ‘conv12_expand_weights’] 三个参数来展示:

    这个脚本仅用了两行代码就完成了 VisualDL-Histogram 的使用,

    vdl_writer = LogWriter(vdl_save_dir)在我们指定的路径创建一个记录器

    vdl_writer.add_histogram把我们想查看的参数直方图记录下来

    !python visualize_params.py
    

    然后我们点击左侧面板的可视化->设置logdir->添加->选择日志目录output/deeplabv3p_mobilenetv3_large_ssld/vdl_out/params_histogram/->启动VisualDL服务,打开VisualDL面板后,点击直方图。

    可视化结果如下所示:

    通过 VisualDL-Service 共享可视化结果

    • 此功能是 VisualDL 2.0.4 新添加的功能,只需要一行代码 visualdl service upload 即可以将自己的log文件上传到远端,

    • 非常推荐这个功能,我们上传文件之后,就不再需要在本地保存这些文件,直接访问生成的链接就可以了,十分方便!

    • 如果你没有安装 VisualDL 2.0.4 ,你需要使用命令pip install visualdl==2.0.4安装

    • 执行下面的代码之后,访问生成的链接, 我也将本项目过程中的某些 log 文件通过此功能上传到了云端, 有需要的话可以进行查看对比;

    注意:当前版本上传时间间隔有 5min 的限制,上传的模型大小有100M的限制

    !pip install visualdl==2.0.4
    

    下面每一行代码下面的注释是对应的我在编写该项目时生成的链接,需要的话直接复制到浏览器就可以进行查看了!

    执行代码时记得把 # 去掉

    #!visualdl service upload --logdir ./output/deeplabv3p_mobilenetv3_large_ssld/vdl_log/
    #训练过程日志: https://paddlepaddle.org.cn/paddle/visualdl/service/app?id=5ed43118ce337581f73e7408b2e1bb41
    
    #!visualdl service upload --logdir ./output/vdl_output/image_transforms/
    #数据增强结果:https://paddlepaddle.org.cn/paddle/visualdl/service/app?id=d55259116534346fda66cf0d43353696
    
    #!visualdl service upload --logdir ./output/deeplabv3p_mobilenetv3_large_ssld/vis_predict_log/
    #预测结果对比:https://paddlepaddle.org.cn/paddle/visualdl/service/app?id=d087973f90ffb93f3ae2ba68d62d3d6e
    
    #!visualdl service upload --model ./output/deeplabv3p_mobilenetv3_large_ssld/best_model/model.pdmodel
    #模型结构:https://paddlepaddle.org.cn/paddle/visualdl/service/app?id=4e79096d61184b8833ce260efe705513
    
    #!visualdl service upload --logdir ./output/deeplabv3p_mobilenetv3_large_ssld/vdl_out/params_histogram/
    #模型参数直方图:https://paddlepaddle.org.cn/paddle/visualdl/service/app?id=6e085d445b640ff45b8c464d46c694ea
    

    结束语

    怎么样?VisualDL是不是很不错呢?快去Github点点Star吧!

    什么?你觉得不太行?点完Star, 去issue里吐槽一下吧,会彳亍起来的!

    想深入了解一下其他功能? 来我的 PaddleSeg 篇看看吧!

    觉得写得不错的话,互相点个关注吧,如果你觉得写的有问题,也欢迎在评论区指正!

    参考链接:

    VisualDL2.0–让PaddleX的模型训练『看』的见: https://aistudio.baidu.com/aistudio/projectdetail/954530

    展开全文
  • 本项目利用 PaddleX 以及 PaddleSeg 两个套件分别实现遥感影像地块分割; 更重要的是想给大家展示一下如何利用VisualDL这个强大的可视化工具来辅助训练及调参; 这个工具非常好用,在模型训练中给了我很大的帮助...
    • 本项目利用 PaddleX 以及 PaddleSeg 两个套件分别实现遥感影像地块分割;

    • 更重要的是想给大家展示一下如何利用VisualDL这个强大的可视化工具来辅助训练及调参;

    这个工具非常好用,在模型训练中给了我很大的帮助,也希望看完本文之后这个工具能给你带来帮助,也希望大家能去GitHub给点一点star,让官方把这个工具做的越来越好

    • 最近百度与CCF合作举办了遥感影像地块分割的比赛,希望该项目可以帮助大家提高成绩;

    • PaddleX 篇 的目的在于重点介绍 VisualDLScalar、Image 功能,以及如何利用它们来辅助我们的训练,其次会简单介绍一下 VisualDL 的其他功能;

    • PaddleSeg 篇的目的在于重点介绍VisualDLGraph、Histogram 功能,以及如何利用它们来帮助我们进行网络模型结构的设计;

    • 两篇都是通过遥感影像地块分割为应用背景,在实战中体验如何应用VisualDL

    背景及工具介绍

    • 遥感影像地块分割 旨在对遥感影像进行像素级内容解析,对遥感影像中感兴趣的类别进行提取和分类。也就是利用图像分割提取出图像中的房屋,水域,农田等用地类型,在城乡规划、防汛救灾等领域具有很高的实用价值
    • PaddleSeg是基于PaddlePaddle开发的端到端图像分割开发套件,覆盖了DeepLabv3+, U-Net, ICNet, PSPNet, HRNet, Fast-SCNN等主流分割网络。通过模块化的设计,以配置化方式驱动模型组合,帮助开发者更便捷地完成从训练到部署的全流程图像分割应用。关于PaddleSeg 的更多信息大家可以去 PaddleSeg GitHub 查看
    • VisualDL 是飞桨可视化分析工具,以丰富的图表呈现训练参数变化趋势、模型结构、数据样本、高维数据分布等。可帮助用户更清晰直观地理解深度学习模型训练过程及模型结构,进而实现高效的模型优化。支持标量、图结构、数据样本可视化、直方图、PR曲线及高维数据降维呈现等诸多功能,同时VisualDL提供可视化结果保存服务。具体细节大家可以自行去 VisualDL Github 主页查看;

    安装PaddleSeg

    • 可以到PaddleSeg GitHub 页面手动下载,也可以使用命令自动下载,但这两种方式可能由于网络原因都会比较慢;我已经将官方下好的压缩包打包好了,这里是地址

    • 需要注意的是,在该项目中我们想要查看数据增强的过程,但 PaddleSeg 并没有提供对应的接口,所以我对 PaddleSeg 做了一些源码上的改动,保证可以达到我们的目的;改动后的版本也打包好了,我已经将其放在了项目中,是基于 PaddleSeg-v0.7.0 修改的;改动的更多细节就不在这里讲了;执行下面的代码即可完成解压安装;如果你不小心将文件删掉了,可以去这个地址下载;

    !unzip work/PaddleSeg.zip -d work/
    
    • 前面的部分我们依旧跟 PaddleX 篇 一致,介绍如何利用 VisualDL-Image 帮助我们选择数据增强策略,利用 VisualDL-Scalar 帮助我们进行训练时超参的调整;

    对于这两个功能,该篇主要是介绍如何在 PaddleSeg 套件中使用,更多策略细节请参考 PaddleX 篇;

    • 随后该篇的重点是利用 VisualDL-Graph,Visual-Histogram 来帮助我们进行网络模型结构的设计;

    设置工作目录

    先切换到工作目录,使用PaddleSeg时我们一般就设置工作目录为 PaddleSeg/,脚本默认都是在该目录下执行的;

    %cd work/PaddleSeg/
    

    获取数据集

    • 我们依旧使用 PaddleX 篇中用到的数据集,原始数据集经过处理之后是可以直接拿来在 PaddleSeg 中用的;关于数据集的更多信息可以去 PaddleX 篇看一下,这里就不介绍了;

    • 关于PaddleSeg数据集的具体要求,大家可以去查看一下官方文档

    • 我也将整理好并且经过脚本校验的数据集公开了,这里是数据集的地址;你可以将该数据集直接添加到项目中来,在项目页面点击右上角的修改,在点开的页面中依次点击 下一步 -> 添加数据集 ,然后在搜索公开数据集的搜索框中搜索:冰河PaddleSeg篇数据集,点击添加后刷新该页面即可

    # 注意这里的地址换成你实际数据集地址,点击左侧 数据集 标签,然后点数据集名称右边的 复制按钮,即可复制这里的路径
    
    !unzip /home/aistudio/data/data61269/land_classify.zip -d dataset/
    

    数据集校验

    • 强烈建议做好数据集后使用官方的校验脚本 pdseg/check.py 来确认一下我们的数据集是没有问题的,关于这个脚本的使用方法,可以参考这里

    • 你如果对这个校验脚本感兴趣的话,可以执行下面的命令再校验一下;但在数据集校验之前你首先需要设置配置文件,PaddleSeg 的脚本基本都是从配置文件中获取信息的;

    • 我们的配置文件一般放在 PaddleSeg/configs 目录下,我创建了一个配置文件 land_classify.yaml ,已经放在了该目录下;关于配置文件的具体细节参考官方文档

    !python pdseg/check.py --cfg configs/land_classify.yaml
    

    下载预训练模型

    • PaddleSeg 提供了丰富的分割预训练模型,在这些模型的基础上使用我们自己的数据集进行微调,一般会使我们的训练更加稳定;

    • 我们在配置文件中设置了预训练模型为:unet_bn_coco

    • 执行下面的脚本即可下载我们需要的预训练模型,关于更多预训练模型的介绍,可以参考这里

    !python pretrained_model/download_model.py unet_bn_coco
    

    利用 VisualDL-Image 选择数据增强策略

    • 在训练开始前,选择一个合适的数据增强策略是至关重要的。数据增强策略选择的好,可以让模型提升很大的效果;

    • 我在知道 VisualDL 之前选择数据增强策略时,完全是跟着感觉走,根本不知道经过增强操作之后的数据长什么样,在其他比赛中,我利用 VisualDL-Image 选择了合适的增强策略,让模型效果提升很多

    • 如何在PaddleSeg中设置增强策略呢?又如何利用VisualDL来帮助我们进行增强策略的选择呢?您接着往下看

    • PaddleSeg中内置了丰富的数据增强策略,完全不需要我们再手动实现一遍,我们只需要在配置文件中进行修改就可以了;

    • 我们的配置文件中选择了下面的增强策略,关于更多的数据增强选择,可以查看这里

    • 偷偷告诉你,MIRROR这个增强你在比赛中一定要用,很关键!

    AUG:
        # 图像resize的方式有三种:
        # unpadding(固定尺寸),stepscaling(按比例resize),rangescaling(长边对齐)
        AUG_METHOD: 'unpadding'
        
        # 图像resize的固定尺寸(宽,高),非负
        FIX_RESIZE_SIZE: (500, 500)
        
        # 图像镜像左右翻转
        MIRROR: True
        # 图像上下翻转开关,True/False
        FLIP: True
        # 图像启动上下翻转的概率,0-1
        FLIP_RATIO: 0.5
    
        RICH_CROP:
            # RichCrop数据增广开关,用于提升模型鲁棒性
            ENABLE: True
            # 图像旋转最大角度,0-90
            MAX_ROTATION: 15
            # 裁取图像与原始图像面积比,0-1
            MIN_AREA_RATIO: 0.5
            # 裁取图像宽高比范围,非负
            ASPECT_RATIO: 0.33
            # 亮度调节范围,0-1
            BRIGHTNESS_JITTER_RATIO: 0.1
            # 饱和度调节范围,0-1
            SATURATION_JITTER_RATIO: 0.1
            # 对比度调节范围,0-1
            CONTRAST_JITTER_RATIO: 0.1
    
    • 执行脚本 pdseg/visualize_seg_transforms.py 即可载入数据并进行增强操作,同时将增强后的结果记录下来;

    • 我们每五张记录一次图片,每十张记录在一个窗口中;日志文件保存在 vdl_log_aug ;

    • 请不要尝试在 reader 未加载完所有数据就直接 return,这样有时候会报错,虽然并不影响 VisualDL-Image 保存图片;

    !python pdseg/visualize_seg_transforms.py --cfg configs/land_classify.yaml --use_gpu
    
    • 之后我们设置 vdl_log_aug 为 logdir, 启动 VisualDL 服务,在 样本数据-样本数据图像 中就可以看到增强后的图片了,每个窗口记录了十张图片,你可以通过点击滑动条的不同部位来查看这十张图片;

    • 你可以根据可视化后的结果,选择最合适的数据增强策略;如果你的训练数据集够大,你可以适当留一些质量不怎么高的图片在数据集中,这样一定程度上可以防止模型过拟合,提升模型的鲁棒性;

    • 注意我在配置文件中选择的增强策略可能是有点问题的,在这一步一定要利用VisualDL-Image调整几次找到最合适的策略;

    • 同样的,我已经利用VisualDL-Service 将这里的日志文件上传了,你可以通过以下链接查看这些图像,关于 VisualDL-Service 的使用,可以参考PaddleX篇最后的部分或者官网,使用起来方便快捷;

    • 数据增强日志文件: https://paddlepaddle.org.cn/paddle/visualdl/service/app?id=84717e4e8e435b33bb779f1521eb71b7

    这里只点开了一张图,每个窗口中我们都存了十张图,同时你还可以看到下面还有很多窗口,都可以一一点开来看;

    利用 VisualDL-Scalar 辅助训练过程

    • 其实 PaddleSeg 的某些地方也集成了 VisualDL 的Scalar、Image 功能, 要想在训练时使用这两个功能,

    • 只需要我们在启动训练时指定参数 --use_vdl 开启功能,并且设置参数vdl_log_dir 设置记录文件保存的目录,就可以使用 Scalar 功能了,你可以查看到 loss曲线lr 曲线,以及 speed曲线

    • 如果在以上的基础上,你还指定了参数--do_eval 在保存检查点时进行一次 evaluate,那么你还会在 eval 之后看到 miou 曲线 以及 macc 曲线

    • 如果在以上的基础上,你还在配置文件中设置了 DATASET:VIS_FILE_LIST,就会在每次做完 evaluate 之后对你这个文件中图像做一次预测结果的可视化, 你可以在 样本数据-样本数据图像 标签中,查看到原图,预测图,标签(如果有标签的话),我不建议你在该文件中指定太多的图片,只需要挑几张具有代表性的图片就可以了,否则会拖慢你的训练速度;我们从val_list.txt 中挑出几行写到 test_list.txt 中;

    • 该篇如何选择合适的学习率就不细讲了,可以参考 PaddleX 篇;值得一提的是,你可以选择在训练的过程中不断调整学习率,而不必使用初始学习率一直到训练结束,具体怎么调整大家可以参考一下其他资料,当然利用VisualDL进行可视化的调整才是最靠谱的,不能只凭他人经验来调整;

    我们指定上述的所有参数来使用内置好的所有功能,关于更多训练参数的指定,参考这里

    !python pdseg/train.py --use_gpu --do_eval --use_vdl --vdl_log_dir 'vdl_log' --cfg configs/land_classify.yaml
    
    • 时间原因我这里只训练了 200epoch,可以看到 UNet 与 Deeplabv3p 的差距还是有的,不过通过曲线可以看出,再训练多一点轮次应该还是有提升的;因为train_loss 还是在不断下降,验证集的准确度也在不断上升的,所以可以尝试再多训练一些轮次看看效果;

    • 训练之后我也利用VisualDL-Service 将训练时的日志上传了,可以参考一下以下链接:https://paddlepaddle.org.cn/paddle/visualdl/service/app?id=ace33d308b4706b45aa25aca5a59531a

    利用VisualDL-Graph查看网络结构

    • 前面选用的是 UNet, UNet结构虽然比较简单,但在数据集样本较少的时候往往也能发挥出很好的效果,是一个非常经典的网络;

    • 我们查看一下训练好的模型结构,点击左侧标签可视化->选择模型文件,选择 saved_model/best_model/model.pdmodel 注意这里要选择到文件,不是文件夹;启动VisualDL 服务后,点击网络结构标签,就可以查看到网络结构了,你可以使用鼠标滑轮随意缩放,查看网络的整体结构或者感兴趣的区域,你还可以在右上角的搜索框中搜索想要查看的节点;点击图中的某个节点后,在右侧即可查看该节点的详细属性,op_callstack 中我们可以看到该操作的调用顺序,也很容易找到这个节点对应的代码在哪;

    • UNet 的突出特点就是做了信息融合,也即图中concat 的部分,你可以从图中清晰的看到通过concat节点将浅层的信息与深层的信息进行了融合;

    • 网络结构也通过 VisualDL-Service 上传了,参考链接:https://paddlepaddle.org.cn/paddle/visualdl/service/app?id=b893ad69c8bcd3996d343b99898ca491

    在修改网络结构之前,一定要清楚修改前的网络结构分别对应哪些部分,你在看的时候要将代码与图对应起来,这样比较容易理解,代码下面就有图哦。

    • 首先看pdseg/models/modeling/unet.py 中的 70行做上采样的部分;代码中75行的 encode()
    def encode(data):
        # 编码器设置
        short_cuts = []
        with scope("encode"):
            with scope("block1"):
                data = double_conv(data, 64)
                short_cuts.append(data)
            with scope("block2"):
                data = down(data, 128)
                short_cuts.append(data)
            with scope("block3"):
                data = down(data, 256)
                short_cuts.append(data)
            with scope("block4"):
                data = down(data, 512)
                short_cuts.append(data)
            with scope("block5"):
                data = down(data, 512)
        return data, short_cuts
    

    上来先做了一个 double_conv, double_conv 的代码就在最开始的部分,也即 conv+bn+relu 做了两次,这是网络最开始的部分,那么我们看图中的网络结构,找最开的部分,非常清楚的可以看到有这么一个结构,我在图上用红色标了出来;

    def double_conv(data, out_ch):
        param_attr = fluid.ParamAttr(
            name='weights',
            regularizer=fluid.regularizer.L2DecayRegularizer(
                regularization_coeff=0.0),
            initializer=fluid.initializer.TruncatedNormal(loc=0.0, scale=0.33))
        with scope("conv0"):
            data = bn_relu(
                conv(data, out_ch, 3, stride=1, padding=1, param_attr=param_attr))
        with scope("conv1"):
            data = bn_relu(
                conv(data, out_ch, 3, stride=1, padding=1, param_attr=param_attr))
        return data
    

    接着看代码,代码随后将处理后的数据进行了保存,这是为了之后做融合用的,先不管它;

    再往后只剩下了重复的四次下采样操作 down(), down的代码就在 double_conv 下面,从43行开始,其中的操作也即 max_pool + double_conv ,在图中我们顺着刚才的地方沿着短线向下看,长线是去做融合的,先不看;

    下面确实是重复的 pool2d + double_conv 的结构, 我在图中标出了第一个结构,剩下的结构你可以沿着往下看,很轻松的可以看到;

    与代码中对应,一共出现了四次这样的结构,直到你看到了一个节点:bilinear_interp

    def down(data, out_ch):
        # 下采样:max_pool + 2个卷积
        with scope("down"):
            data = max_pool(data, 2, 2, 0)
            data = double_conv(data, out_ch)
        return data
    
    • 这之后就进入了 decode的部分了,我们接着看decode部分的代码,就在 encode 的下面,从96行开始,
    def decode(data, short_cuts):
        # 解码器设置,与编码器对称
        with scope("decode"):
            with scope("decode1"):
                data = up(data, short_cuts[3], 256)
            with scope("decode2"):
                data = up(data, short_cuts[2], 128)
            with scope("decode3"):
                data = up(data, short_cuts[1], 64)
            with scope("decode4"):
                data = up(data, short_cuts[0], 64)
        return data
    

    我们看到代码中做了四次上采样操作up() ,up()的代码从51行开始,这里先有一个上采样操作的选择,resize_bilinear 或者 deconv,从图中我们可以看到,选择了 resize_bilinear;

    再往后有一个concat操作,这里就是UNet做信息融合的部分了, 紧接着又是一个double_conv,

    总结一下 up() 里的操作就是 resize_bilinear + concat + double_conv, 而decode中做了四次 up(),从图中我们也可以很清楚的看到出现了四次 concat,

    每一个concat 有两个输入,一个就是短线,是经过 resize_bilinear 后的数据;另一个就是长线,是之前保存的浅层信息;我在图中画出了第一个 up 结构;

    def up(data, short_cut, out_ch):
        # 上采样:data上采样(resize或deconv), 并与short_cut concat
        param_attr = fluid.ParamAttr(
            name='weights',
            regularizer=fluid.regularizer.L2DecayRegularizer(
                regularization_coeff=0.0),
            initializer=fluid.initializer.XavierInitializer(),
        )
        with scope("up"):
            if cfg.MODEL.UNET.UPSAMPLE_MODE == 'bilinear':
                data = fluid.layers.resize_bilinear(data, short_cut.shape[2:])
            else:
                data = deconv(
                    data,
                    out_ch // 2,
                    filter_size=2,
                    stride=2,
                    padding=0,
                    param_attr=param_attr)
            data = fluid.layers.concat([data, short_cut], axis=1)
            data = double_conv(data, out_ch)
        return data
    
    • 接下来就剩一个操作了,看代码 decode() 完了之后做了一个get_logit(),此函数的代码 从110行开始,其中只有一个卷积操作,也就对应图上第四个 up() 之后的节点 conv2d
    def get_logit(data, num_classes):
        # 根据类别数设置最后一个卷积层输出
        param_attr = fluid.ParamAttr(
            name='weights',
            regularizer=fluid.regularizer.L2DecayRegularizer(
                regularization_coeff=0.0),
            initializer=fluid.initializer.TruncatedNormal(loc=0.0, scale=0.01))
        with scope("logit"):
            data = conv(
                data, num_classes, 3, stride=1, padding=1, param_attr=param_attr)
        return data
    

    代码结束了,但图中还有一部分内容,下面这些节点分为两部分,一部分是 tanspose2 + arg_max + unsqueeze2 这里是生成分割后的结果pred

    另一部分就是剩下的所有节点了,都是用来计算 loss 的,有兴趣的话可以去loss.py 中对照着查看一下,也比较清晰;

    利用VisualDL-Graph修改网络结构

    • 怎么样,通过 VisualDL 的可视化功能是不是让你对模型结构有了更深的认识呢?代码配上VisualDL-Graph 也变得更加容易理解了;

    • 通过上面的训练我们可以看到,相较于PaddleX 篇的 Deeplabv3p,UNet的表现有一点差;关于 UNet 的改进已经有了各种各样的尝试,在AIStudio上也有很多优秀的项目,我们主要参考 Zhou_Lu 大佬的项目:被玩坏的U-Net结构 来进行一些修改,利用VisualDL-Graph 帮助我们理解代码逻辑,并验证模型结构是否正确;利用ResNet做替换,主要就是替换掉 encode 的部分,利用到 ResNet 的特征提取能力以及缓解梯度消失与梯度爆炸的优点;

    • 利用 ResNet 作为 UNet 的 backbone, 在 pdseg/models/backbone 我们新建了一个 resnet_for_unet.py 来作为backbone,但是刚才我们也看了 unet.py 中并没有调整 backbone 的代码,所以我们需要修改 unet.py;我们直接参照大佬项目中的 PaddleSeg_base/pdseg/models/modeling/unet.py 来修改,我们这里只使用 resnet,修改好的结构我放在了 pdseg/models/modeling/unet_resnet.py 中。

    • 新的配置文件为 unet_resnet.yaml,我们先修改训练轮次为 1 ;查看一下网络结构;我已经将网络结构通过 VisualDL 上传了,

    你可以通过链接来查看网络结构:https://paddlepaddle.org.cn/paddle/visualdl/service/app?id=a2469150d68a0cfefabd0921aa2911fa

    • 注意这部分也要结合图来看哦,代码下面就是图

    • 我们是将 UNet 的 encode部分替换成了ResNet,decode 后面的部分并没有改变,打开上面的连接后,我们点击网络结构,

    直接拖到最下面,我们从下向上查看网络结构,先是很熟悉的计算 loss 的部分,这里有一点点不一样,多了一个 resize_bilinear,查看节点信息定位到代码 unet_resnet.py 139 行,这里是大佬自己加上的一个操作;

    • 接着往上看图,我们可以看到 get_logit 那个卷积操作,接着是四个 up() 操作,再往上就是我们替换的 ResNet 的部分了,我们结合代码看一下:

    • 从 resnet_for_unet.py 中第51 行开始定义操作,首先是一个 conv_bn_layer,这是在 98 行定义的,也就是说这里 conv_bn_layer = conv + bn + relu

    def conv_bn_layer(self,
                          input,
                          num_filters,
                          filter_size,
                          stride=1,
                          groups=1,
                          act=None,
                          name=None,
                          data_format='NCHW'):
            conv = fluid.layers.conv2d(
                input=input,
                num_filters=num_filters,
                filter_size=filter_size,
                stride=stride,
                padding=(filter_size - 1) // 2,
                groups=groups,
                act=None,
                param_attr=ParamAttr(name=name + "_weights"),
                bias_attr=False,
                name=name + '.conv2d.output.1',
                data_format=data_format)
    
            if name == "conv1":
                bn_name = "bn_" + name
            else:
                bn_name = "bn" + name[3:]
            return fluid.layers.batch_norm(
                input=conv,
                act=act,
                name=bn_name + '.output.1',
                param_attr=ParamAttr(name=bn_name + '_scale'),
                bias_attr=ParamAttr(bn_name + '_offset'),
                moving_mean_name=bn_name + '_mean',
                moving_variance_name=bn_name + '_variance',
                data_layout=data_format)
    
    • 随后紧跟着一个 append 操作,这里保存节点的操作我想你应该知道是用来干嘛的了,没错,就是融合;反映在图上也一定有一条长线连接到后面的某个 concat 操作去,你可以自己验证一下;

    • 紧接着是一个 pool2d 操作,我们选择了 layers=50, 到循环里是就是在重复做 bottleneck_block 操作,并且保存了每次操作之后的节点;循环次数也很好算 3+4+6+3 = 16;

    我们查看 145行的 bottleneck_block, 包含 conv0 + conv1 + conv2 + short_cut + elementwise_add + relu

    其中

    conv0 = conv1 = conv + bn + relu;

    conv2 = conv + bn ;

    short_cut = conv2 or 不做改动

    def bottleneck_block(self, input, num_filters, stride, name, data_format):
            conv0 = self.conv_bn_layer(
                input=input,
                num_filters=num_filters,
                filter_size=1,
                act='relu',
                name=name + "_branch2a",
                data_format=data_format)
            conv1 = self.conv_bn_layer(
                input=conv0,
                num_filters=num_filters,
                filter_size=3,
                stride=stride,
                act='relu',
                name=name + "_branch2b",
                data_format=data_format)
            conv2 = self.conv_bn_layer(
                input=conv1,
                num_filters=num_filters * 4,
                filter_size=1,
                act=None,
                name=name + "_branch2c",
                data_format=data_format)
    
            short = self.shortcut(
                input,
                num_filters * 4,
                stride,
                is_first=False,
                name=name + "_branch1",
                data_format=data_format)
    
            return fluid.layers.elementwise_add(
                x=short, y=conv2, act='relu', name=name + ".add.output.5")
    

    我们在图上很容易找到这么一个部分,我已经在图上画出了第一个部分,bottleneck1 中的shortcut 为conv2 , bottleneck2 中的 shortcut 不做改动,因为图比较长,只放了最上面的部分,你可以点击链接查看完整的网络结构;

    按我们的计算,应该可以找到连续 16个这样的结构, 并且在第3,3+4, 3+4+6, 3+4+6+3 个结构之后分别都会有一条长线连接到后面的concat操作

    你可以自己验证一下,这样整个网络我们就看完了,是不是觉得很清晰呢?如果你想用ResNet的其他版本来替换UNet,可以参考大佬项目中代码,按照这种方式更快更清晰的理解代码;

    • 理论上 ResNet 的各种变种都可以用来替换 UNet 的 encode,按照这里的代码思路把该保存的浅层信息保存下来就可以了,主要都是这个 bottleneck 部分;

    利用VisualDL-Scalar 辅助新模型的训练

    • 下面所有参数与之前保持一致,指定新的配置文件 unet_resnet.yaml 开始新的训练;注意:因为之前的预训练模型是基于UNet的,所以在加载权重的时候会跳过很多,我们替换后网络并没有这些权重;

    • 因为更改网络之后 收敛速度变慢了,在训练200epoch后 loss还没有降到UNet 同期水平,我多训练了 100个epoch还是没有降下来,再训练了100epoch 还是没降下来,收敛速度有点堪忧,但整体还是在下降的;

    • 我在重复训练的过程中并没有指定新的 vdl_log_dir 这样在启动 VisualDL 时只会读取最新时间戳的那个 log,我写了个脚本 pdseg/classify_log.py 可以将该文件夹下的log文件放到不同的文件夹下,有需要的话参考下面行代码进行调整:

    !python pdseg/classify_log.py --vdl_log_dir vdl_log_unet_resnet/

    请注意不要在训练的过程中使用这个脚本,因为训练过程会不断记录log,在你移动log文件后,又会自动产生新的 log;

    • 分开 log 之后,我通过 VisualDL-Service 上传了这几次训练的log,大家可以参考一下,点击右侧的Wall time 就可以清晰的对比了:

    https://paddlepaddle.org.cn/paddle/visualdl/service/app?id=f5ad6fd804e5bb5273f449106348f5d3

    !python pdseg/train.py --use_gpu --do_eval --use_vdl --vdl_log_dir 'vdl_log_unet_resnet' --cfg configs/unet_resnet.yaml
    

    这里是训练时完整的大图,可以看到每张图都有三种颜色的线,就对应我们的三次训练, loss 在不断下降,macc 跟 miou 在不断上升

    • 鉴于以上模型 train_loss 下降慢的情况,可能的原因有:
    1. 训练数据中存在脏数据:回头再通过我分享的链接查看一下增强后的图片,好像确实质量有点低,可能存在一定的影响,心急吃不了热豆腐,开始训练前一定要利用 VisualDL-Image 保证数据是高质量的;

    2. 可能训练轮次不够:观察loss曲线,确实还有下降的趋势,在保证数据质量之后或许可以增大轮次试一下效果;

    3. 可能存在梯度消失的情况,也就是说可能我们自己的网络设计的不合理:下面我们就通过 VisualDL-Histogram 查看一下是否真的有不合理的地方

    利用VisualDL-Histogram 查看参数直方图

    • 不同于标量 Scalar,Tensor 通常是多维度的,也就无法直接用曲线图的形式展现出来;

    • 我们可以通过 VisualDL-Histogram 功能查看 Tensor 的直方图数据在训练过程中的变化趋势,来深入了解模型各层效果,从而可以精准的调优模型;

    • 我写好了一个脚本 pdseg/visualize_seg_params.py, 我们可以通过上面网络结构查看一些 Tensor 的详细信息,取出 输入/Filter/name 在脚本中替换 vis_var_names 进行查看

    !python pdseg/visualize_seg_params.py
    

    这里很多结构的输入张量的 weights 来展示,具体选了哪些大家可以去脚本中查看一下;

    如果你选取了别的参数,并且打开VisualDL没有看到直方图的标签,那么很有可能是你的参数名称写错了;

    直方图是一个个切片叠加起来的,其中的颜色有深浅,颜色越深表示时间越早,也就是最初的几个epoch;

    针对一个切片,横轴表示其值,纵轴表示数量,如下图,鼠标悬停的点表示以 以0.001为中心的bin中有333个元素

    一般权重正常的话直方图就比较类似这种形状,如果某些权重出现了非常平或者非常集中,也就是说形状类似一条竖线或者一条横线,就表明网络可能出问题了;有时候某些权重多轮之后没有太大改变,也是异常的情况;

    我们通过 VisualDL-Histogram 就能准确定位出现问题的地方,从而快速的进行调整;

    下面是部分截图,完整的直方图参考链接:

    https://paddlepaddle.org.cn/paddle/visualdl/service/app?id=8298d39fcaa79f0e1aaba4a32e56fe28

    通过上图我们可以看到,我们的网络结构应该是正常的,如果你想看看不正常的直方图长什么样,可以设计一个非常深的网络,应该就可以看出差距了;

    总结一下,

    本篇首先我们修改了 PaddleSeg 的一部分代码,使其能够使用 VisualDL-Image 来查看数据增强效果;

    接着我们使用了 VisualDL-Scalar 来辅助我们的UNet训练;

    随后我们通过 VisualDL-Graph 结合代码加深了我们对网络结构的理解,并在此基础上借助可视化,我们很方便的使用 ResNet 替换了 UNet 的 backbone,并再次利用VisualDL-Scalar辅助了这个新网络的训练;

    最后我们通过 VisualDL-Histogram 查看了网络设计是否存在异常;

    同时贯穿全篇的是我们使用了 VisualDL-Service 进行了可视化结果的分享;

    相信在整个过程中你也能感受到 VisualDL 的可视化功能给我们带来的便利,同时也知道了如何在 PaddleSeg 这个套件中使用 VisualDL,赶快在你以后的项目中利用起来吧。

    结束语

    怎么样?VisualDL是不是很不错呢?快去Github点点Star吧!

    什么?你觉得不太行?点完Star, 去issue里吐槽一下吧,会彳亍起来的!

    想深入了解 PaddleX 如何应用VisualDL? 来我的 PaddleX 篇看看吧!

    觉得写得不错的话,互相点个关注吧,如果你觉得写的有问题,也欢迎在评论区指正!

    展开全文
  • 常规赛:遥感影像地块分割实战 文章目录常规赛:遥感影像地块分割实战赛题介绍数据说明赛题任务训练数据集提交内容及格式提交示例第一步 环境配置第二步 解压数据集数据预处理读取语义 分割任务数据集模型训练继续...

    常规赛:遥感影像地块分割实战

    本人项目源码传送门
    https://aistudio.baidu.com/aistudio/projectdetail/1809354

    比赛页面传送门: 常规赛:遥感影像地块分割

    欢迎参加 飞桨领航团实战速成营

    赛题介绍

    本赛题由 2020 CCF BDCI 遥感影像地块分割 初赛赛题改编而来。遥感影像地块分割, 旨在对遥感影像进行像素级内容解析,对遥感影像中感兴趣的类别进行提取和分类,在城乡规划、防汛救灾等领域具有很高的实用价值,在工业界也受到了广泛关注。现有的遥感影像地块分割数据处理方法局限于特定的场景和特定的数据来源,且精度无法满足需求。因此在实际应用中,仍然大量依赖于人工处理,需要消耗大量的人力、物力、财力。本赛题旨在衡量遥感影像地块分割模型在多个类别(如建筑、道路、林地等)上的效果,利用人工智能技术,对多来源、多场景的异构遥感影像数据进行充分挖掘,打造高效、实用的算法,提高遥感影像的分析提取能力。 赛题任务 本赛题旨在对遥感影像进行像素级内容解析,并对遥感影像中感兴趣的类别进行提取和分类,以衡量遥感影像地块分割模型在多个类别(如建筑、道路、林地等)上的效果。

    数据说明

    本赛题提供了多个地区已脱敏的遥感影像数据,各参赛选手可以基于这些数据构建自己的地块分割模型。

    赛题任务

    本赛题旨在对遥感影像进行像素级内容解析,并对遥感影像中感兴趣的类别进行提取和分类,以衡量遥感影像地块分割模型在多个类别(如建筑、道路、林地等)上的效果。

    训练数据集

    样例图片及其标注如下图所示:

    训练数据集文件名称:train_and_label.zip
    包含2个子文件,分别为:训练数据集(原始图片)文件、训练数据集(标注图片)文件,详细介绍如下:

    训练数据集(原始图片)文件名称:img_train
    包含66,653张分辨率为2m/pixel,尺寸为256 * 256的JPG图片,每张图片的名称形如T000123.jpg。

    训练数据集(标注图片)文件名称:lab_train
    包含66,653张分辨率为2m/pixel,尺寸为256 * 256的PNG图片,每张图片的名称形如T000123.png。
    备注: 全部PNG图片共包括4种分类,像素值分别为0、1、2、3。此外,像素值255为未标注区域,表示对应区域的所属类别并不确定,在评测中也不会考虑这部分区域。

    测试数据集
    测试数据集文件名称:img_test.zip,详细介绍如下:
    包含4,609张分辨率为2m/pixel,尺寸为256 * 256的JPG图片,文件名称形如123.jpg。

    提交内容及格式

    • 以zip压缩包形式提交结果文件,文件命名为 result.zip;
    • zip压缩包中的图片格式必须为单通道PNG;
    • PNG文件数需要与测试数据集中的文件数相同,且zip压缩包文件名需要与测试数据集中的文件名一一对应;
    • 单通道PNG图片中的像素值必须介于0~3之间,像素值不能为255。如果存在未标注区域,评测系统会自动忽略对应区域的提交结果。

    提交示例

    提交文件命名为:result.zip,zip文件的组织方式如下所示:

    主目录
    ├── 1.png #每个结果文件命名为:测试数据集图片名称+.png
    ├── 2.png
    ├── 3.png
    ├── …

    备注: 主目录中必须包含与测试数据集相同数目、名称相对应的单通道PNG图片,且每张单通道PNG图片中的像素值必须介于0~3之间,像素值不能为255。

    第一步 环境配置

    !pip install paddlex -i https://mirror.baidu.com/pypi/simple
    !pip install imgaug -i https://mirror.baidu.com/pypi/simple
    
    # 设置使用0号GPU卡(如无GPU,执行此代码后仍然会使用CPU训练模型)
    import matplotlib
    import os
    import paddlex as pdx
    
    os.environ['CUDA_VISIBLE_DEVICES'] = '0'
    
    import numpy as np
    from paddlex.seg import transforms
    import imgaug.augmenters as iaa
    
    from tqdm import tqdm
    import cv2
    
    
    /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddle/fluid/layers/utils.py:26: DeprecationWarning: `np.int` is a deprecated alias for the builtin `int`. To silence this warning, use `int` by itself. Doing this will not modify any behavior and is safe. When replacing `np.int`, you may wish to use e.g. `np.int64` or `np.int32` to specify the precision. If you wish to review your current use, check the release note link for additional information.
    Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations
      def convert_to_list(value, n, name, dtype=np.int):
    

    第二步 解压数据集

    解压数据集并读取信息

    !unzip -q data/data77571/train_and_label.zip
    !unzip -q data/data77571/img_test.zip
    
    !rm -rf __MACOSX 
    
    import numpy as np
    
    datas = []
    image_base = 'img_train'   # 训练集原图路径
    annos_base = 'lab_train'   # 训练集标签路径
    
    # 读取原图文件名
    ids_ = [v.split('.')[0] for v in os.listdir(image_base)]
    
    # 将训练集的图像集和标签路径写入datas中
    for id_ in ids_:
        img_pt0 = os.path.join(image_base, '{}.jpg'.format(id_))
        img_pt1 = os.path.join(annos_base, '{}.png'.format(id_))
        datas.append((img_pt0.replace('/home/aistudio', ''), img_pt1.replace('/home/aistudio', '')))
        if os.path.exists(img_pt0) and os.path.exists(img_pt1):
            pass
        else:
            raise "path invalid!"
    
    # 打印datas的长度和具体存储例子
    print('total:', len(datas))
    print(datas[0][0])
    print(datas[0][1])
    print(datas[0][:])
    
    total: 66652
    img_train/T007604.jpg
    lab_train/T007604.png
    ('img_train/T007604.jpg', 'lab_train/T007604.png')
    
    import numpy as np
    
    # 四类标签,这里用处不大,比赛评测是以0、1、2、3类来对比评测的
    labels = ['0', '1', '2',  '3']
    
    # 将labels写入标签文件
    with open('labels.txt', 'w') as f:
        for v in labels:
            f.write(v+'\n')
    
    # 随机打乱datas
    np.random.seed(5)
    np.random.shuffle(datas)
    
    # 验证集与训练集的划分,0.10表示10%为训练集,90%为训练集
    split_num = int(0.10*len(datas))
    
    # 划分训练集和验证集
    train_data = datas[:-split_num]
    valid_data = datas[-split_num:]
    
    # 写入训练集list
    with open('train_list.txt', 'w') as f:
        for img, lbl in train_data:
            f.write(img + ' ' + lbl + '\n')
    
    # 写入验证集list
    with open('valid_list.txt', 'w') as f:
        for img, lbl in valid_data:
            f.write(img + ' ' + lbl + '\n')
    
    # 打印训练集和测试集大小
    print('train:', len(train_data))
    print('valid:', len(valid_data))
    
    train: 59987
    valid: 6665
    

    数据预处理

    from paddlex.seg import transforms
    import imgaug.augmenters as iaa
    
    # 定义训练和验证时的transforms
    train_transforms = transforms.Compose([
        transforms.RandomHorizontalFlip(),
        transforms.Resize(target_size=300),
        transforms.RandomPaddingCrop(crop_size=256),
        transforms.RandomBlur(prob=0.1),
        transforms.RandomRotate(rotate_range=15),
        # transforms.RandomDistort(brightness_range=0.5),
        transforms.Normalize()
    ])
    eval_transforms = transforms.Compose([
        transforms.Resize(256),
        transforms.Normalize()
    ])
    

    读取语义 分割任务数据集

    data_dir = './'
    
    # 定义训练和验证数据集
    train_dataset = pdx.datasets.SegDataset(
        data_dir=data_dir,                # 数据集路径
        file_list='train_list.txt',       # 训练集图片文件list路径
        label_list='labels.txt',          # 训练集标签文件list路径
        transforms=train_transforms,      # train_transforms
        shuffle=True)                     # 数据集是否打乱
        
    eval_dataset = pdx.datasets.SegDataset(
        data_dir=data_dir,                # 数据集路径
        file_list='valid_list.txt',       # 验证集图片文件list路径
        label_list='labels.txt',          # 验证集标签文件list路径
        transforms=eval_transforms)       # eval_transforms
    
    2021-05-24 09:47:22 [INFO]	59987 samples in file train_list.txt
    2021-05-24 09:47:22 [INFO]	6665 samples in file valid_list.txt
    

    模型训练

    运行时长: 1天8小时10分钟43秒259毫秒

    利用paddlex.seg.DeepLabv3p 构建 DeepLabv3p分割器。

    import paddle
    # 分割类别数
    num_classes = len(train_dataset.labels)
    
    # 构建DeepLabv3p分割器
    model = pdx.seg.DeepLabv3p(
        num_classes=num_classes,  backbone='Xception65', use_bce_loss=False
    )
    
    visualdl = paddle.callbacks.VisualDL(log_dir='visualdl_log')
    
    # 模型训练
    model.train(
        num_epochs=100,                 # 训练迭代轮数
        train_dataset=train_dataset,  # 训练集读取
        train_batch_size=32,           # 训练时批处理图片数
        eval_dataset=eval_dataset,    # 验证集读取
        learning_rate=0.01,         # 学习率
        save_interval_epochs=4,       # 保存模型间隔轮次  
        save_dir='output',    # 模型保存路径
        log_interval_steps=1000,       # 日志打印间隔
        pretrain_weights='output/best_model',
        use_vdl=True)  #加载预训练模型
    

    在这里插入图片描述

    继续训练(可选)

    主要是怕运行过程中蹦了,不想从头再来

    model = pdx.load_model(./output/epoch_100)
    
    # 模型训练
    model.train(
        num_epochs=100,                 # 训练迭代轮数
        train_dataset=train_dataset,  # 训练集读取
        train_batch_size=32,           # 训练时批处理图片数
        eval_dataset=eval_dataset,    # 验证集读取
        learning_rate=0.01,         # 学习率
        save_interval_epochs=4,       # 保存模型间隔轮次  
        save_dir='output',    # 模型保存路径
        log_interval_steps=1000,       # 日志打印间隔
        pretrain_weights='output/best_model',
        use_vdl=True)  #加载预训练模型
    

    模型评估

    # 加载模型
    model = pdx.load_model('./output/best_model')
    
    # 模型评估
    model.evaluate(eval_dataset, batch_size=16, epoch_id=None, return_details=False)
    
    /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddle/fluid/layers/math_op_patch.py:298: UserWarning: /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddlex/cv/nets/xception.py:316
    The behavior of expression A + B has been unified with elementwise_add(X, Y, axis=-1) from Paddle 2.0. If your code works well in the older versions but crashes in this version, try to use elementwise_add(X, Y, axis=0) instead of A + B. This transitional warning will be dropped in the future.
      op_type, op_type, EXPRESSION_MAP[method_name]))
    
    
    2021-05-25 18:09:00 [INFO]	Model[DeepLabv3p] loaded.
    2021-05-25 18:09:00 [INFO]	Start to evaluating(total_samples=6665, total_steps=417)...
    
    
    100%|██████████| 417/417 [00:47<00:00,  8.83it/s]
    
    
    
    
    
    OrderedDict([('miou', 0.6151651914567804),
                 ('category_iou',
                  array([0.61855467, 0.76549378, 0.60033287, 0.47627945])),
                 ('oacc', 0.7767960595081118),
                 ('category_acc',
                  array([0.78364765, 0.84085465, 0.73642031, 0.67315414])),
                 ('kappa', 0.6934252160032077),
                 ('category_F1-score',
                  array([0.76432966, 0.86717245, 0.75026   , 0.64524294]))])
    

    模型预测

    from tqdm import tqdm
    import cv2
    
    test_base = 'img_testA/'    # 测试集路径
    out_base = 'result/'        # 预测结果保存路径
    
    # 是否存在结果保存路径,如不存在,则创建该路径
    if not os.path.exists(out_base):
        os.makedirs(out_base)
    
    # 模型预测并保存预测图片
    for im in tqdm(os.listdir(test_base)):
        if not im.endswith('.jpg'):
            continue
        pt = test_base + im
        result = model.predict(pt)
        cv2.imwrite(out_base+im.replace('jpg', 'png'), result['label_map'])
    
    100%|██████████| 4608/4608 [01:19<00:00, 57.66it/s]
    

    小结

    项目离MIoU高分还差很远。

    具体的话可以再改进一下两点

    1.数据预处理部分,加入更好的数据增强方案。
    例如将本实验得到的结果数据0-3都扩大一定的倍数
    (不放大全是黑图,人眼难以辨别)
    查看一下图片显示那些存在明显的问题,人为去看一些有哪些通病,再去进行适当的预处理。

    2.选用更合理的训练的轮次和其他等等训练参数。

    3.当然,查阅了前面高分大佬的作品,也可以直接采用paddle的OCRNET进行预测,效果会比本项目好一些。

    我在AI Studio上获得黄金等级,点亮7个徽章,来互关呀~

    https://aistudio.baidu.com/aistudio/personalcenter/thirdview/643467

    展开全文
  • 【Baseline】CCF遥感影像地块分割(非官方)使用DeepLabV3+ 使用DeepLabV3+ !pip install paddlex -i https://mirror.baidu.com/pypi/simple !pip install imgaug -i https://mirror.baidu.com/pypi/simple # ...
  • 赛道B 遥感图像地块分割与提取赛题:代码: 赛题: 耕地的数量和质量是保持农业可持续发展的关键,利用卫星遥感影像可以识别并提取耕地,并对耕地进行遥感制图,准确的耕地分布能够为国家决策部门提供重要支撑。目前...
  • 地块分割本质上是个语义分割的问题,初赛考虑7类结果的平均IOU,复赛加入了对水体和道路类的连通性评判指标。IoU是一种测量在特定数据集中检测相应物体准确度的一个标准。只要是在输出中得出一个预测范围(bounding ...
  •  赛题分析 赛题分析 这次比赛也是第一次正式参加的比赛,地块分割本质上也是个语义分割的问题,之前没搞过,花了比较多的时间对网上的trick进行了整理学习,见最后附录整理的资料。初赛并不困难,主要考虑7类结果的...

空空如也

空空如也

1 2 3 4 5 ... 11
收藏数 213
精华内容 85
关键字:

遥感影像地块分割