背景及工具介绍
如果你是一个新手,在使用飞桨成熟的套件完成任务的同时,会不会好奇使用的网络长什么样呢?网络在套件中又是如何实现的呢?
本项目首先会介绍 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
通过下面的代码就可以一键下载了,下载好的预训练模型也在该目录下
!python pretrained_model/download_model.py pspnet50_bn_cityscapes
下载好的模型权重参数为分散的文件,我们需要将其导出为推理模型,利用 pdseg/export_model.py 就可以完成了;但是该脚本需要指定一个配置文件,我们利用内置的配置文件 configs/pspnet_optic.yaml,首先需要下载数据集;
然后指定参数修改配置文件,执行下面的代码就可以完成了:
#下载数据集!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 篇看看吧!
觉得写得不错的话,互相点个关注吧,如果你觉得写的有问题,也欢迎在评论区指正!