背景及工具介绍
如果你是一个新手,在使用飞桨成熟的套件完成任务的同时,会不会好奇使用的网络长什么样呢?网络在套件中又是如何实现的呢?
本项目首先会介绍 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