使用VisualDL可视化模型:PSPNet

背景及工具介绍

如果你是一个新手,在使用飞桨成熟的套件完成任务的同时,会不会好奇使用的网络长什么样呢?网络在套件中又是如何实现的呢?

本项目首先会介绍 PSPNet,然后利用 VisualDL-Graph 可视化模型网络结构功能,看一看 PSPNet 到底长什么样,代码又是如何实现的,帮助大家更好的理解 PSPNet,同时使用了 VisualDL-Service 来共享可视化结果;

PaddleSeg中已经实现了很多分割网络,其中就包含我们今天的主角:PSPNet,我们今天就通过 VisualDL-Graph 来看一看 PSPNet 是如何实现的;

VisualDL 是飞桨可视化分析工具,以丰富的图表呈现训练参数变化趋势、模型结构、数据样本、高维数据分布等。可帮助用户更清晰直观地理解深度学习模型训练过程及模型结构,进而实现高效的模型优化。支持标量、图结构、数据样本可视化、直方图、PR曲线及高维数据降维呈现等诸多功能,同时VisualDL提供可视化结果保存服务。具体细节大家可以自行去 VisualDL Github 主页查看;

这个可视化工具是非常好用的,也是训练中必不可少的,关于 VisualDL 其他功能如何在项目中使用,可以参考我的其他文章;

最后也希望大家能够去 Github 上点一点star,让官方能把这个工具做的越来越好!

安装 PaddleSeg

我将官方的 PaddleSeg-v0.7.0 下载好了,已经挂载在项目中,这里直接解压安装,并切换至静态图默认工作目录 PaddleSeg/

如果项目中没有的话,搜索 公开数据集 PaddleSeg-v0.7.0 就可以找到了

!unzip /home/aistudio/data/data60663/PaddleSeg-release-v0.7.0.zip -d work/
!mv work/PaddleSeg-release-v0.7.0/ work/PaddleSeg
%cd work/PaddleSeg/

下载预训练模型并导出

PaddleSeg 提供了丰富的预训练模型,我们想要查看 PSPNet 的网络结构,首先需要下载一个 PSPNet 的预训练模型,我这里选择了:pspnet50_bn_cityscapes

通过 PaddleSeg/pretrained_model/download_model.py 就可以一键下载了,下载好的预训练模型也在该目录下

!python pretrained_model/download_model.py pspnet50_bn_cityscapes

下载好的模型权重参数为分散的文件,我们需要将其导出为推理模型,利用 pdseg/export_model.py 就可以完成了;

但是该脚本需要指定一个配置文件,我们利用内置的配置文件 configs/pspnet_optic.yaml,首先需要下载数据集;

然后指定参数修改配置文件,DATASET.NUM_CLASSES 改为 19; TEST.TEST_MODEL 改为 “./pretrained_model/pspnet50_bn_cityscapes/”

# 下载数据集
!python dataset/download_optic.py
# 更改配置文件参数,导出推理模型
推理模型
!python pdseg/export_model.py --cfg configs/pspnet_optic.yaml DATASET.NUM_CLASSES 19 TEST.TEST_MODEL  "./pretrained_model/pspnet50_bn_cityscapes/"

PSPNet 介绍

百度之前开过一门图像分割的课程,图像分割七日打卡营,课程中介绍了一些主流的分割网络,推荐大家去看一看;

先贴一张论文中截图,这张图很清晰的展示了 PSPNet 在 FCN 的基础上解决了什么问题:

我们看图像第一行,FCN 会把船识别为车,因为这张图中的船与车的外观很像,但是PSPNet 并没有误识别,因为其金字塔模块利用了上下文信息,周围有水的情况下,这应该是一艘船;

也就是感受野的问题,PSPNet 通过不同 scale 的金字塔进行处理,也就是图中红黄蓝绿四个部分,最后再将不同尺度的结果进行 concat;

在这之前,需要利用 ResNet 提取图像特征;关于 ResNet 大家可以参考一下其他资料,我们下面只看一下实现的代码,原理就不细说了;

PaddleSeg 静态图实现的网络在 PaddleSeg/pdseg/models/modeling 目录下,其中有 fast_scnn, pspnet, deeplab, unet等;

我们查看 pspnet.py 的内容:

从第107开始是模型的定义,其中有四个部分,首先是使用 ResNet 作为 backbone, 然后就是一个 PSP 模块, 紧跟着有一个 dropout 层,最后是一个 get_logit_interp 得到原尺寸的输出;

def pspnet(input, num_classes):
    # Backbone: ResNet
    res = resnet(input)
    # PSP模块
    psp = psp_module(res, 512)
    dropout = fluid.layers.dropout(psp, dropout_prob=0.1, name="dropout")
    # 根据类别数决定最后一层卷积输出, 并插值回原始尺寸
    logit = get_logit_interp(dropout, num_classes, input.shape[2:])
    return logit

利用 VisualDL-Graph 查看模型网络结构

接下来我们结合模型网络结构图,分别查看一下这四个部分的内容,

我们点击左侧标签 可视化->选择模型文件->选择 work/PaddleSeg/freeze_model/__ model __ ->启动VisualDL服务 -> 打开VisualDL,在打开的网页中就可以看到我们的网络结构了

如果你在本地有模型文件,把文件直接拖入页面就可以进行加载了,十分方便

Backbone: ResNet

首先是第一个模块,也即网络的backbone: ResNet

在 pspnet.py 中我们可以看到结构的定义,也就是下面的代码,首先从配置文件中获取了 scale 和 layers,然后从 resnet_backbone 中获取了模型;

def resnet(input):
    # PSPNET backbone: resnet, 默认resnet50
    # end_points: resnet终止层数
    # dilation_dict: resnet block数及对应的膨胀卷积尺度
    scale = cfg.MODEL.PSPNET.DEPTH_MULTIPLIER
    layers = cfg.MODEL.PSPNET.LAYERS
    end_points = layers - 1
    dilation_dict = {2: 2, 3: 4}
    model = resnet_backbone(layers, scale, stem='pspnet')
    data, _ = model.net(
        input, end_points=end_points, dilation_dict=dilation_dict)

    return data

PaddleSeg 的 backbone 文件都在PaddleSeg/pdseg/models/backbone 目录下,我们找到 resnet.py, 第49行开始net函数开始就是backbone的实现;

开始是一些参数的设定,直到第88行开始,首先是3个 conv_bn_layer 操作,

conv = self.conv_bn_layer(
                input=input,
                num_filters=int(64 * self.scale),
                filter_size=3,
                stride=2,
                act='relu',
                name="conv1_1")

conv_by_layer 的操作从209行开始,里面有两个操作,224行的 conv = fluid.layers.conv2d 以及 241行的 fluid.layers.batch_norm,结合第88行调用的部分,我们可以得到操作为:

conv2d + batch_norm + relu , 我们看网络结构图一开始的地方,应该能看到3个这样的结构:

但是,这里多了个 elementwise_add 操作,这是因为在 conv2d 中指定了参数 bias_attr;

接着看resnet.py 的代码, 3个conv_bn_layer操作之后,到 119行有一个 conv = fluid.layers.pool2d,看图中第三个conv_bn_layer 之后确实有一个 pool2d

Backbone: ResNet

接下来你可以先在网络结构页面滚动滑轮,进行缩放,你会发现之后的部分比较有规律,结构都比较相似,结合 resnet.py 第133行开始,我们发现进入了一个循环,

for block in range(len(depth)):
	for i in range(depth[block]):

因为我们的layers选择的是50,结合第80行代码,我们可以得到depth = [3, 4, 6, 3] ,所以可以得到这里的循环为 3 + 4 + 6 + 3 次,我们看一下循环体:

其中主要的就是145行的 conv = self.bottleneck_block,它的定义在258行开始:

def bottleneck_block(self, input, num_filters, stride, name, dilation=1):
        if self.stem == 'pspnet' and self.layers == 101:
            strides = [1, stride]
        else:
            strides = [stride, 1]

        conv0 = self.conv_bn_layer(
            input=input,
            num_filters=num_filters,
            filter_size=1,
            dilation=1,
            stride=strides[0],
            act='relu',
            name=name + "_branch2a")
        if dilation > 1:
            conv0 = self.zero_padding(conv0, dilation)
        conv1 = self.conv_bn_layer(
            input=conv0,
            num_filters=num_filters,
            filter_size=3,
            dilation=dilation,
            stride=strides[1],
            act='relu',
            name=name + "_branch2b")
        conv2 = self.conv_bn_layer(
            input=conv1,
            num_filters=num_filters * 4,
            dilation=1,
            filter_size=1,
            act=None,
            name=name + "_branch2c")

        short = self.shortcut(
            input,
            num_filters * 4,
            stride,
            is_first=False,
            name=name + "_branch1")

        return fluid.layers.elementwise_add(
            x=short, y=conv2, act='relu', name=name + ".add.output.5")

可以看到,首先是三个 conv_bn_layer,这个结构上面已经讲过了,接着是一个 shortcut, 其定义从251行开始:

def shortcut(self, input, ch_out, stride, is_first, name):
        ch_in = input.shape[1]
        if ch_in != ch_out or stride != 1 or is_first == True:
            return self.conv_bn_layer(input, ch_out, 1, stride, name=name)
        else:
            return input

可以看到这个函数要么直接返回 input,要么返回一个 conv_bn_layer 操作,最后是一个 fluid.layers.elementwise_add 将此函数的返回结果与第三个 conv_bn_layer (conv2)相加,注意 conv2 中的 act=None 也即没有进行 relu 操作, shortcut 中也一样没有relu

因为这里有两种返回结果的可能,所以你可以想象图中就会出现两种不同结构的 bottleneck_block, 先总结一下:

bottleneck_block = conv_bn_layer with relu * 2 + conv_bn_layer without relu + conv_bn_layer without relu or input + elementwise_add

我们接着刚才的 pool2d 往下看图,

在图中你应该可以看到上面讲过的 conv_bn_layer, 根据上面的分析这就是一种 bottleneck_block,其中 shortcut 的返回是一个 conv_bn_layer without relu,我们称其为 bottleneck_block_0;

再往下看图:

这就是另一种 bottleneck_block,其中 shortcut 直接返回 input, elementwise_add 将 input 直接与 conv2 的结果相加, 我们称其为 bottleneck_block_1;

再往下看图,出现了一个重复的结构,这是意料之中的,按照我们的分析确实会有重复的结构出现 3 + 4 + 6 + 3 次,以上我们已经过完了三个 bottleneck_block: bottleneck_block_0 + bottleneck_block_1 * 2

可以想到再往下会出现类似的四个 bottleneck_block,我们看图,确实出现了 bottleneck_block_0 + bottleneck_block_1 * 3,

这里由于分辨率的原因,我的截图不够清晰,大家可以去自己的页面对照一下,同时可以想到的是之后还会出现 bottleneck_block_0 + bottleneck_block_1 * 5 以及 bottleneck_block_0 + bottleneck_block_1 * 2

至此,循环的部分就结束了,我们回到 resnet.py 第166行,还剩下 fluid.layers.pool2d 以及 fluid.layers.fc,但是我们在网络结构中并没有发现这两个操作,这是resnet进行分类的层,在分割中不需要用到;

PSP模块

backbone的部分终于看完了,接下来就是 psp 模块,在 pspnet.py 中49行开始:

def psp_module(input, out_features):
    # Pyramid Scene Parsing 金字塔池化模块
    # 输入:backbone输出的特征
    # 输出:对输入进行不同尺度pooling, 卷积操作后插值回原始尺寸,并concat
    #       最后进行一个卷积及BN操作

    cat_layers = []
    sizes = (1, 2, 3, 6)
    for size in sizes:
        psp_name = "psp" + str(size)
        with scope(psp_name):
            pool = fluid.layers.adaptive_pool2d(
                input,
                pool_size=[size, size],
                pool_type='avg',
                name=psp_name + '_adapool')
            data = conv(
                pool,
                out_features,
                filter_size=1,
                bias_attr=True,
                name=psp_name + '_conv')
            data_bn = bn(data, act='relu')
            interp = fluid.layers.resize_bilinear(
                data_bn, out_shape=input.shape[2:], name=psp_name + '_interp')
        cat_layers.append(interp)
    cat_layers = [input] + cat_layers[::-1]
    cat = fluid.layers.concat(cat_layers, axis=1, name='psp_cat')

    psp_end_name = "psp_end"
    with scope(psp_end_name):
        data = conv(
            cat,
            out_features,
            filter_size=3,
            padding=1,
            bias_attr=True,
            name=psp_end_name)
        out = bn(data, act='relu')

    return out

我们可以看到其中也有一个循环,for size in sizes 其中 sizes = (1, 2, 3, 6),也即循环四次,每次取出1,2,3,6 作为参数;这也就是上面提到的四种 scale 的金字塔结构;

循环中的操作为:fluid.layers.adaptive_pool2d + conv + bn + fluid.layers.resize_bilinear,也即我们应该在图中能看到 4 个类似的结构,我们在图中接着backbone结束的部分向下看,

很清楚的能看到这样一个结构,再向下看代码,循环结束有一个 concat 操作,上图中也可以看到;

最后是一个 conv + bn,我们看图:

这样 PSP 模块的部分就结束了;

剩余模块

再往下还有两个部分, dropout + get_logit_interp,这次我们先看图,然后再去验证代码是不是一样的:

接着bn结束的地方往下看图,我们看到一个dropout,dropout 后面应该就是get_logit_interp了,我们看到操作应该为:conv + fluid.layers.resize_bilinear

之后的部分 transpose 等应该就是后处理的部分了,我们去代码中验证一下,get_logit_interp的定义在28行:

def get_logit_interp(input, num_classes, out_shape, name="logit"):
    # 根据类别数决定最后一层卷积输出, 并插值回原始尺寸
    param_attr = fluid.ParamAttr(
        name=name + 'weights',
        regularizer=fluid.regularizer.L2DecayRegularizer(
            regularization_coeff=0.0),
        initializer=fluid.initializer.TruncatedNormal(loc=0.0, scale=0.01))

    with scope(name):
        logit = conv(
            input,
            num_classes,
            filter_size=1,
            param_attr=param_attr,
            bias_attr=True,
            name=name + '_conv')
        logit_interp = fluid.layers.resize_bilinear(
            logit, out_shape=out_shape, name=name + '_interp')
    return logit_interp

后处理的代码在 work/PaddleSeg/pdseg/models/model_builder.py 中 第233行 logit = softmax(logit) 其中softmax 的定义在 96行

def softmax(logit):
    logit = fluid.layers.transpose(logit, [0, 2, 3, 1])
    logit = fluid.layers.softmax(logit)
    logit = fluid.layers.transpose(logit, [0, 3, 1, 2])
    return logit

与上图一致,transpose + softmax + transpose, 最后的 scale 是导出推理模型的操作;

利用VisualDL-Service共享可视化结果

  • 此功能是 VisualDL 2.0.4 新添加的功能,你需要安装 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 分享了出来,大家直接复制下面的链接打开网页就可以查看了;

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

!visualdl service upload --model freeze_model/__model__

这样整个 PSPNet 的大致代码我们就看完了,你可以结合模型网络结构图再整体回顾一下,有没有觉得结合 VisualDL-Graph 可视化,代码看起来非常好懂呢?

每一部分的代码实现的是网络的哪一部分是不是也一目了然呢?同时通过 VisualDL-Service 生成一个链接就实现了可视化结果共享,是不是很方便呢?

如果你有其他感兴趣的网络或者搞不懂的网络,结合 VisualDL-Graph 看一看网络长什么样吧,我相信你一定会很快理解的!

其实 VisualDL强大之处远不止于此,其他功能的使用可以参考的我的其他文章哦,赶快用起来 VisualDL 吧!
小提示:去AIStudio查看此项目更舒爽~

结束语

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

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

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

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

参考链接:

图像分割七日打卡营:https://aistudio.baidu.com/aistudio/course/introduce/1767

PSPNet 论文:https://arxiv.org/abs/1612.01105

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值