憨批的语义分割重制版4——TF2 搭建自己的PSPNet语义分割平台

学习前言

还是要弄一个TF2版本的,虽然和Keras没啥差别
在这里插入图片描述

什么是PSPNet模型

PSPNet模型最主要的特点是采用了PSP模块。

该模型提出的金字塔池化模块(Pyramid Pooling Module)能够聚合不同区域的上下文信息,从而提高获取全局信息的能力。实验表明这样的先验表示(即指代PSP这个结构)是有效的,在多个数据集上展现了优良的效果。

PSP结构的功能是将获取到的特征层划分成不同大小的网格,每个网格内部各自进行平均池化。实现聚合不同区域的上下文信息,从而提高获取全局信息的能力。

在PSPNet中,PSP结构典型情况下,会将输入进来的特征层划分成6x6,3x3,2x2,1x1的网格,对应了图片中的绿色、蓝色、橙色、红色的的输出
在这里插入图片描述
其中:
红色:将输入进来的特征层整个进行平均池化。
橙色:将输入进来的特征层划分为2×2个子区域,然后对每个子区域进行平均池化。
蓝色:将输入进来的特征层划分为3×3个子区域,然后对每个子区域进行平均池化。
绿色:将输入进来的特征层划分为6×6个子区域,然后对每个子区域进行平均池化。

代码下载

Github源码下载地址为:
https://github.com/bubbliiiing/pspnet-tf2

PSPNet实现思路

一、预测部分

1、主干网络介绍

在这里插入图片描述
PSPNet在论文中采用的是Resnet系列作为主干特征提取网络,本博客会给大家提供两个主干网络,分别是resnet50和mobilenetv2。

但是由于算力限制(我没有什么卡),为了方便博客的进行,本文以mobilenetv2为例,给大家进行解析。

关于mobilenetv2的介绍大家可以看我的另外一篇博客https://blog.csdn.net/weixin_44791964/article/details/102851214

MobileNet模型是Google针对手机等嵌入式设备提出的一种轻量级的深层神经网络,其使用的核心思想便是depthwise separable convolution。

MobileNetV2是MobileNet的升级版,它具有两个特征点:

1、Inverted residuals,在ResNet50里我们认识到一个结构,bottleneck design结构,在3x3网络结构前利用1x1卷积降维,在3x3网络结构后,利用1x1卷积升维,相比直接使用3x3网络卷积效果更好,参数更少,先进行压缩,再进行扩张。而在MobileNetV2网络部分,其采用Inverted residuals结构,在3x3网络结构前利用1x1卷积升维,在3x3网络结构后,利用1x1卷积降维,先进行扩张,再进行压缩
在这里插入图片描述

2、Linear bottlenecks,为了避免Relu对特征的破坏,在在3x3网络结构前利用1x1卷积升维,在3x3网络结构后,再利用1x1卷积降维后,不再进行Relu6层,直接进行残差网络的加法。
在这里插入图片描述
整体网络结构如下,该图是针对输入为(224,224,3)的图片而言的:(其中bottleneck进行的操作就是上述的创新操作)
在这里插入图片描述
需要注意的是,在PSPNet当中,一般不会5次下采样,可选的有3次下采样和4次下采样,本文使用的4次下采样这里所提到的下采样指的是不会进行五次长和宽的压缩,通常选用三次或者四次长和宽的压缩。

from tensorflow.keras.models import Model
from tensorflow.keras import layers
from tensorflow.keras.layers import Input
from tensorflow.keras.layers import Lambda
from tensorflow.keras.layers import Activation
from tensorflow.keras.layers import Concatenate
from tensorflow.keras.layers import Add
from tensorflow.keras.layers import Dropout
from tensorflow.keras.layers import BatchNormalization
from tensorflow.keras.layers import Conv2D
from tensorflow.keras.layers import DepthwiseConv2D
from tensorflow.keras.layers import ZeroPadding2D
from tensorflow.keras.layers import GlobalAveragePooling2D
from tensorflow.keras.activations import relu
import tensorflow as tf

def _make_divisible(v, divisor, min_value=None):
    if min_value is None:
        min_value = divisor
    new_v = max(min_value, int(v + divisor / 2) // divisor * divisor)
    if new_v < 0.9 * v:
        new_v += divisor
    return new_v

def relu6(x):
    return relu(x, max_value=6)

def _inverted_res_block(inputs, expansion, stride, alpha, in_filters, filters, block_id, skip_connection, rate=1):
    pointwise_conv_filters = int(filters * alpha)
    pointwise_filters = _make_divisible(pointwise_conv_filters, 8)
    x = inputs
    prefix = 'expanded_conv_{}_'.format(block_id)
    if block_id:
        # Expand
        x = Conv2D(expansion * in_filters, kernel_size=1, padding='same',
                   use_bias=False, activation=None,
                   name=prefix + 'expand')(x)
        x = BatchNormalization(epsilon=1e-3, momentum=0.999,
                               name=prefix + 'expand_BN')(x)
        x = Activation(relu6, name=prefix + 'expand_relu')(x)
    else:
        prefix = 'expanded_conv_'
    # Depthwise
    x = DepthwiseConv2D(kernel_size=3, strides=stride, activation=None,
                        use_bias=False, padding='same', dilation_rate=(rate, rate),
                        name=prefix + 'depthwise')(x)
    x = BatchNormalization(epsilon=1e-3, momentum=0.999,
                           name=prefix + 'depthwise_BN')(x)

    x = Activation(relu6, name=prefix + 'depthwise_relu')(x)

    # Project
    x = Conv2D(pointwise_filters,
               kernel_size=1, padding='same', use_bias=False, activation=None,
               name=prefix + 'project')(x)
    x = BatchNormalization(epsilon=1e-3, momentum=0.999,
                           name=prefix + 'project_BN')(x)

    if skip_connection:
        return Add(name=prefix + 'add')([inputs, x])

    # if in_filters == pointwise_filters and stride == 1:
    #    return Add(name='res_connect_' + str(block_id))([inputs, x])

    return x

def get_mobilenet_encoder(inputs_size, downsample_factor=8):
    if downsample_factor == 16:
        block4_dilation = 1
        block5_dilation = 2
        block4_stride = 2
    elif downsample_factor == 8:
        block4_dilation = 2
        block5_dilation = 4
        block4_stride = 1
    else:
        raise ValueError('Unsupported factor - `{}`, Use 8 or 16.'.format(downsample_factor))
    
    # 473,473,3
    inputs = Input(shape=inputs_size)

    alpha=1.0
    first_block_filters = _make_divisible(32 * alpha, 8)
    # 473,473,3 -> 237,237,32
    x = Conv2D(first_block_filters,
                kernel_size=3,
                strides=(2, 2), padding='same',
                use_bias=False, name='Conv')(inputs)
    x = BatchNormalization(
        epsilon=1e-3, momentum=0.999, name='Conv_BN')(x)
    x = Activation(relu6, name='Conv_Relu6')(x)

    # 237,237,32 -> 237,237,16
    x = _inverted_res_block(x, in_filters=32, filters=16, alpha=alpha, stride=1,
                            expansion=1, block_id=0, skip_connection=False)

    #---------------------------------------------------------------#
    # 237,237,16 -> 119,119,24
    x = _inverted_res_block(x, in_filters=16, filters=24, alpha=alpha, stride=2,
                            expansion=6, block_id=1, skip_connection=False)
    x = _inverted_res_block(x, in_filters=24, filters=24, alpha=alpha, stride=1,
                            expansion=6, block_id=2, skip_connection=True)
                            
    #---------------------------------------------------------------#
    # 119,119,24 -> 60,60.32
    x = _inverted_res_block(x, in_filters=24, filters=32, alpha=alpha, stride=2,
                            expansion=6, block_id=3, skip_connection=False)
    x = _inverted_res_block(x, in_filters=32, filters=32, alpha=alpha, stride=1,
                            expansion=6, block_id=4, skip_connection=True)
    x = _inverted_res_block(x, in_filters=32, filters=32, alpha=alpha, stride=1,
                            expansion=6, block_id=5, skip_connection=True)

    #---------------------------------------------------------------#
    # 60,60,32 -> 30,30.64
    x = _inverted_res_block(x, in_filters=32, filters=64, alpha=alpha, stride=block4_stride,
                            expansion=6, block_id=6, skip_connection=False)
    x = _inverted_res_block(x, in_filters=64, filters=64, alpha=alpha, stride=1, rate=block4_dilation,
                            expansion=6, block_id=7, skip_connection=True)
    x = _inverted_res_block(x, in_filters=64, filters=64, alpha=alpha, stride=1, rate=block4_dilation,
                            expansion=6, block_id=8, skip_connection=True)
    x = _inverted_res_block(x, in_filters=64, filters=64, alpha=alpha, stride=1, rate=block4_dilation,
                            expansion=6, block_id=9, skip_connection=True)

    # 30,30.64 -> 30,30.96
    x = _inverted_res_block(x, in_filters=64, filters=96, alpha=alpha, stride=1, rate=block4_dilation,
                            expansion=6, block_id=10, skip_connection=False)
    x = _inverted_res_block(x, in_filters=96, filters=96, alpha=alpha, stride=1, rate=block4_dilation,
                            expansion=6, block_id=11, skip_connection=True)
    x = _inverted_res_block(x, in_filters=96, filters=96, alpha=alpha, stride=1, rate=block4_dilation,
                            expansion=6, block_id=12, skip_connection=True)
    # 辅助分支训练
    f4 = x

    #---------------------------------------------------------------#
    # 30,30.96 -> 30,30,160 -> 30,30,320
    x = _inverted_res_block(x, in_filters=96, filters=160, alpha=alpha, stride=1, rate=block4_dilation,  # 1!
                            expansion=6, block_id=13, skip_connection=False)
    x = _inverted_res_block(x, in_filters=160, filters=160, alpha=alpha, stride=1, rate=block5_dilation,
                            expansion=6, block_id=14, skip_connection=True)
    x = _inverted_res_block(x, in_filters=160, filters=160, alpha=alpha, stride=1, rate=block5_dilation,
                            expansion=6, block_id=15, skip_connection=True)

    x = _inverted_res_block(x, in_filters=160, filters=320, alpha=alpha, stride=1, rate=block5_dilation,
                            expansion=6, block_id=16, skip_connection=False)
    f5 = x
    return inputs, f4, f5

2、加强特征提取结构

在这里插入图片描述
PSPNet所使用的加强特征提取结构是PSP模块。

PSP结构的做法是将获取到的特征层划分成不同大小的区域,每个区域内部各自进行平均池化。实现聚合不同区域的上下文信息,从而提高获取全局信息的能力。

在PSPNet中,PSP结构典型情况下,会将输入进来的特征层划分成6x6,3x3,2x2,1x1的区域,然后每个区域内部各自进行平均池化。

假设PSP结构输入进来的特征层为30x30x320,此时这个特征层的高和宽均为30,如果我们要将这个特征层划分成6x6的区域,只需要使得平均池化的步长stride=30/6=5和kernel_size=30/6=5就行了,此时的平均池化相当于将特征层划分成6x6的区域,每个区域内部各自进行平均池化。

当PSP结构输入进来的特征层为30x30x320时,PSP结构的具体构成如下。
在这里插入图片描述

def pool_block(feats, pool_factor, out_channel):
	h = K.int_shape(feats)[1]
	w = K.int_shape(feats)[2]
	# strides = [30,30],[15,15],[10,10],[5,5]
	# poolsize 30/6=5 30/3=10 30/2=15 30/1=30
	pool_size = strides = [int(np.round(float(h)/pool_factor)),int(np.round(float(w)/pool_factor))]
	# 进行不同程度的平均
	x = AveragePooling2D(pool_size , data_format=IMAGE_ORDERING , strides=strides, padding='same')(feats)
	# 进行卷积
	x = Conv2D(out_channel//4, (1 ,1), data_format=IMAGE_ORDERING, padding='same', use_bias=False)(x)
	x = BatchNormalization()(x)
	x = Activation('relu' )(x)
	x = Lambda(lambda x: tf.compat.v1.image.resize_images(x, (K.int_shape(feats)[1], K.int_shape(feats)[2]), align_corners=True))(x)
	return x

3、利用特征获得预测结果

利用1、2步,我们可以获取输入进来的图片的特征,此时,我们需要利用特征获得预测结果。

利用特征获得预测结果的过程可以分为3步:
1、利用一个3x3卷积对特征进行整合。
2、利用一个1x1卷积进行通道调整,调整成Num_Classes。
3、利用resize进行上采样使得最终输出层,宽高和输入图片一样。

在这里插入图片描述

from tensorflow.keras.models import *
from tensorflow.keras.layers import *
from nets.mobilenetv2 import get_mobilenet_encoder
from nets.resnet50 import get_resnet50_encoder
import tensorflow.keras.backend as K
import tensorflow as tf
import numpy as np

IMAGE_ORDERING = 'channels_last'
MERGE_AXIS = -1

def pool_block(feats, pool_factor, out_channel):
	h = K.int_shape(feats)[1]
	w = K.int_shape(feats)[2]
	# strides = [30,30],[15,15],[10,10],[5,5]
	# poolsize 30/6=5 30/3=10 30/2=15 30/1=30
	pool_size = strides = [int(np.round(float(h)/pool_factor)),int(np.round(float(w)/pool_factor))]
	# 进行不同程度的平均
	x = AveragePooling2D(pool_size , data_format=IMAGE_ORDERING , strides=strides, padding='same')(feats)
	# 进行卷积
	x = Conv2D(out_channel//4, (1 ,1), data_format=IMAGE_ORDERING, padding='same', use_bias=False)(x)
	x = BatchNormalization()(x)
	x = Activation('relu' )(x)
	x = Lambda(lambda x: tf.compat.v1.image.resize_images(x, (K.int_shape(feats)[1], K.int_shape(feats)[2]), align_corners=True))(x)
	return x

def pspnet(n_classes, inputs_size, downsample_factor=8, backbone='mobilenet', aux_branch=True):
	if backbone == "mobilenet":
		img_input, f4, o = get_mobilenet_encoder(inputs_size, downsample_factor=downsample_factor)
		out_channel = 320
	elif backbone == "resnet50":
		img_input, f4, o = get_resnet50_encoder(inputs_size, downsample_factor=downsample_factor)
		out_channel = 2048
	else:
		raise ValueError('Unsupported backbone - `{}`, Use mobilenet, resnet50.'.format(backbone))
	#-------------------------------------#
	#	PSP模块
	#	分区域进行池化
	#-------------------------------------#
	pool_factors = [1,2,3,6]
	pool_outs = [o]

	for p in pool_factors:
		pooled = pool_block(o, p, out_channel)
		pool_outs.append(pooled)
	
	# 连接
	# 60x60xout_channel*2
	o = Concatenate(axis=MERGE_AXIS)(pool_outs)

	#-------------------------------------#
	#	利用特征获得预测结果
	#-------------------------------------#
	# 卷积
	# 60x60xout_channel//4
	o = Conv2D(out_channel//4, (3,3), data_format=IMAGE_ORDERING, padding='same', use_bias=False)(o)
	o = BatchNormalization()(o)
	o = Activation('relu')(o)
	# 正则化,防止过拟合
	o = Dropout(0.1)(o)

	# 60x60x21
	o = Conv2D(n_classes,(1,1),data_format=IMAGE_ORDERING, padding='same')(o)
	# [473,473,nclasses]
	o = Lambda(lambda x: tf.compat.v1.image.resize_images(x, (inputs_size[1], inputs_size[0]), align_corners=True))(o)
	# 获得每一个像素点属于每一个类的概率了
	o = Activation("softmax", name="main")(o)
	
	if aux_branch:
		f4 = Conv2D(out_channel//8, (3,3), data_format=IMAGE_ORDERING, padding='same', use_bias=False)(f4)
		f4 = BatchNormalization()(f4)
		f4 = Activation('relu')(f4)
		# 防止过拟合
		f4 = Dropout(0.1)(f4)

		# 60x60x21
		f4 = Conv2D(n_classes,(1,1),data_format=IMAGE_ORDERING, padding='same')(f4)
		# [473,473,nclasses]
		f4 = Lambda(lambda x: tf.compat.v1.image.resize_images(x, (inputs_size[1], inputs_size[0]), align_corners=True))(f4)
		# 获得每一个像素点属于每一个类的概率了
		f4 = Activation("softmax", name="aux")(f4)
		model = Model(img_input,[f4,o])
		return model
	else:
		model = Model(img_input,[o])
		return model

二、训练部分

1、训练文件详解

我们使用的训练文件采用VOC的格式。
语义分割模型训练的文件分为两部分。
第一部分是原图,像这样:
在这里插入图片描述
第二部分标签,像这样:
在这里插入图片描述
原图就是普通的RGB图像,标签就是灰度图或者8位彩色图。

原图的shape为[height, width, 3],标签的shape就是[height, width],对于标签而言,每个像素点的内容是一个数字,比如0、1、2、3、4、5……,代表这个像素点所属的类别。

语义分割的工作就是对原始的图片的每一个像素点进行分类,所以通过预测结果中每个像素点属于每个类别的概率与标签对比,可以对网络进行训练。

2、LOSS解析

本文所使用的LOSS由两部分组成:
1、Cross Entropy Loss。
2、Dice Loss。

Cross Entropy Loss就是普通的交叉熵损失,当语义分割平台利用Softmax对像素点进行分类的时候,进行使用。

Dice loss将语义分割的评价指标作为Loss,Dice系数是一种集合相似度度量函数,通常用于计算两个样本的相似度,取值范围在[0,1]。

计算公式如下:
在这里插入图片描述
就是预测结果和真实结果的交乘上2,除上预测结果加上真实结果。其值在0-1之间。越大表示预测结果和真实结果重合度越大。所以Dice系数是越大越好。

如果作为LOSS的话是越小越好,所以使得Dice loss = 1 - Dice,就可以将Loss作为语义分割的损失了。
实现代码如下:

def dice_loss_with_CE(beta=1, smooth = 1e-5):
    def _dice_loss_with_CE(y_true, y_pred):
        y_pred = K.clip(y_pred, K.epsilon(), 1.0 - K.epsilon())

        CE_loss = - y_true[...,:-1] * K.log(y_pred)
        CE_loss = K.mean(K.sum(CE_loss, axis = -1))

        tp = K.sum(y_true[...,:-1] * y_pred, axis=[0,1,2])
        fp = K.sum(y_pred         , axis=[0,1,2]) - tp
        fn = K.sum(y_true[...,:-1], axis=[0,1,2]) - tp

        score = ((1 + beta ** 2) * tp + smooth) / ((1 + beta ** 2) * tp + beta ** 2 * fn + fp + smooth)
        score = tf.reduce_mean(score)
        dice_loss = 1 - score
        # dice_loss = tf.Print(dice_loss, [dice_loss, CE_loss])
        return CE_loss + dice_loss
    return _dice_loss_with_CE

训练自己的PSPNet模型

首先前往Github下载对应的仓库,下载完后利用解压软件解压,之后用编程软件打开文件夹。
注意打开的根目录必须正确,否则相对目录不正确的情况下,代码将无法运行。

一定要注意打开后的根目录是文件存放的目录。
在这里插入图片描述

一、数据集的准备

本文使用VOC格式进行训练,训练前需要自己制作好数据集,如果没有自己的数据集,可以通过Github连接下载VOC12+07的数据集尝试下。
训练前将图片文件放在VOCdevkit文件夹下的VOC2007文件夹下的JPEGImages中。
训练前将标签文件放在VOCdevkit文件夹下的VOC2007文件夹下的SegmentationClass中。

在这里插入图片描述

二、数据集的处理

在完成数据集的摆放之后,我们需要对数据集进行下一步的处理,目的是获得训练用的train.txt以及val.txt,需要用到根目录下的voc_annotation.py。

如果下载的是我上传的voc数据集,那么就不需要运行根目录下的voc_annotation.py。
如果是自己制作的数据集,那么需要运行根目录下的voc_annotation.py,从而生成train.txt和val.txt。
在这里插入图片描述

三、开始网络训练

通过voc_annotation.py我们已经生成了train.txt以及val.txt,此时我们可以开始训练了。训练的参数较多,大家可以在下载库后仔细看注释,其中最重要的部分依然是train.py里的num_classes。

num_classes用于指向检测类别的个数+1!训练自己的数据集必须要修改!

除此之外在train.py文件夹下面,选择自己要使用的主干模型backbone、预训练权重model_path和下采样因子downsample_factor。预训练模型需要和主干模型相对应。下采样因子可以在8和16中选择。
在这里插入图片描述
之后就可以开始训练了。

四、训练结果预测

训练结果预测需要用到两个文件,分别是pspnet.py和predict.py。
我们首先需要去pspnet.py里面修改model_path以及num_classes,这两个参数必须要修改。

model_path指向训练好的权值文件,在logs文件夹里。
num_classes指向检测类别的个数+1。

在这里插入图片描述
完成修改后就可以运行predict.py进行检测了。运行后输入图片路径即可检测。

  • 9
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 34
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 34
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bubbliiiing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值