轻量级语义分割网络 BiseNetv2学习:手把手教你搭建BiseNetv2(tensorflow2),并在Cityscapes上完成语义分割任务

轻量级语义分割网络 BiseNetv2学习:手把手教你搭建BiseNetv2(tensorflow2),并在Cityscapes上完成语义分割任务

前言

博主在之前从头搭建BiseNet的博客(BiseNet学习:利用tensorflow2搭建BiseNet并训练完成语义分割任务_theworld666的博客-CSDN博客)中提到过认识BiseNetv2的契机,是在一次调研语义分割任务中,BiseNetv2作为非常优秀的轻量级语义分割的网络被我选中用于测试,那时的我由于啥都不懂最终什么都没跑出来,在最近学习深度学习后,膨胀了,我个人觉得能自己搭建神经网络,就不必被他人限制系统运行的环境,从而环境由我个人决定,于是在阅读作者论文,和提供的代码,我个人使用tensorflow2版本从头到尾复现了作者所说的网络,并在CityScapes数据集上完成了训练任务。(作者使用tensorflow1版本,我用的是tensorflow2,这两个版本的差别简直是天差地别,导致我看代码的时候总是需要搜索引擎,制作本次博客不易,感觉有帮助的麻烦点个赞)。
。。。
这是六个月后的博主,那个代码发邮箱太累了,大家上git自取吧: https://github.com/liujiawen-jpg/AIleraning

1.网络搭建

BiseNet又称双边分割网络,一般架构是,一边是一个较为粗糙的特征提取网络作为一边,而另一边可以由经典的特征提取网络(EfficientNet,resNet,xception)组成,

在这里插入图片描述

而我们今天的主角BiseNetv2,也类似该架构,在上面(也就是DetailBranch)是一个较为简单的特征提取网络,我们先从他开始构造,他在作者提供的架构图中是这样的(k是卷积核大小,c是输出单元数,s是步长,r是该层重复次数)

1.1DetailBranch的构建

在这里插入图片描述

也就是一个卷积神经网络,不断卷积维度不断增强,随着卷积层步长的调节,图片不断减小(这里要选择填充,不然你的图片大小会由于不填充而产生不符合预期的尺寸错误,从而导致错误,这里的卷积层自然不是单纯的卷积层,而是Conv+BN+Relu的经典组合,BN层的加入可以加速训练),那么说了这么多,我就直接贴代码了

#先构造卷积块,以便我接下来多次运用卷积层的时候可以复用
class ConvBlock(layers.Layer):
    def __init__(self,units,kenral_size,strides,use_activation=True):
        super(ConvBlock,self).__init__()
        self.conv2d=layers.Conv2D(units,kernel_size=kenral_size,strides=strides,padding='same')
        self.bn=layers.BatchNormalization()
        self.ua=use_activation
    def call(self,input):
        x=self.conv2d(input)
        x=self.bn(x)
        if self.ua==True:
            x=tf.nn.relu(x)
        return x
 
#这个细节分支非常简单,在每次卷积减少图片大小之后,后面在跟上不改变图像大小的卷积层用于提取特征
class DetailBranch(layers.Layer):
    def __init__(self):
        super(DetailBranch,self).__init__()
        self.s1Conv1=ConvBlock(units=64,kenral_size=3,strides=2)
        self.s1Conv2=ConvBlock(units=64,kenral_size=3,strides=1)
        self.s2Conv1=ConvBlock(units=64,kenral_size=3,strides=2)
        self.s2Conv2=ConvBlock(units=64,kenral_size=3,strides=1)
        self.s2Conv3=ConvBlock(units=64,kenral_size=3,strides=1)
        self.s3Conv1=ConvBlock(units=128,kenral_size=3,strides=2)
        self.s3Conv2=ConvBlock(units=128,kenral_size=3,strides=1)
        self.s3Conv3=ConvBlock(units=128,kenral_size=3,strides=1)
    def call(self,input):
        x=self.s1Conv1(input)
        x=self.s1Conv2(x)
        x=self.s2Conv1(x)
        x=self.s2Conv2(x)
        x=self.s2Conv3(x)
        x=self.s3Conv1(x)
        x=self.s3Conv2(x)
        x=self.s3Conv3(x)
        return x

这样一个非常简单的,DetailBranch我们就自定义好了,他用于提取图片特征最终将图片大小缩小在原来的1/8,我们可以检验一下

in1=keras.Input(shape=(256,256,3))
db=DetailBranch()
db(in1)

在这里插入图片描述

可以看到在经过网络输出后,数据大小缩小为原来1/8,维度为128层

1.2 SemanticBranch的构建

在这里插入图片描述

在这里语义分支作者有说明其实可以替换为其他的各种经典网络(resNet这些),但是作者在这里阐述了三种特殊的网络构造也可以用于代替经典网络,作为语义分支的主体,并且最终效果会更好,那么于是我个人也选择了作者提供的网络老作为主体,可以看到他们的名称简写分别为Stem,GE,CE,我们一个个介绍。

1.2.1 StemBlock的创建

Stem也就是StemBlock,论文中说:“它采用两种不同的下采样方式来缩小特征表示。然后将两个分支的输出特征串联起来作为输出。该结构具有高效的计算成本和有效的特征表达能力”。他的结构如下

在这里插入图片描述

也就是说stemBlock可以兼顾计算成本和特征提取,我们会将他作为分割分支的第一个特征提取块,图片输入后,经过卷积后,分为两个分支后,最后将经过两层卷积块后的输出与经过卷积核大小为3的最大池化层concate在一起,再卷积作为我们的输出,在这里作者给的这张图非常详细(甚至把每一步的输出都说明了,十分详细),那么我们也就可以直接照着这张图构造StemBlock了

class StemBlock(layers.Layer):
    def __init__(self,channels=16):
        super(StemBlock,self).__init__()
        self.conv1=ConvBlock(units=channels,kenral_size=3,strides=2)
        self.conv2=ConvBlock(units=channels//2,kenral_size=1,strides=1)
        self.conv3=ConvBlock(units=channels,kenral_size=3,strides=2)
        self.conv4=ConvBlock(units=channels,kenral_size=3,strides=1)
        self.maxpool=layers.MaxPool2D(pool_size=3,strides=2,padding='same')#这里注意一下padding一定要设置为‘same'不然输出数据大小会较小
    def call(self,input):
        x=self.conv1(input)
        x1=self.maxpool(x)#分支经过最大池化层后输出
        x2=self.conv2(x)
        x2=self.conv3(x2)#分支经过两层卷积后输出
        x3=tf.concat([x2,x1],axis=-1)
        #这里将两个分支的输出在最后一个维度融合在一起
        x3=self.conv4(x3)#最后一层卷积作为我们的输出
        return x3

那么到了这步作为分割分支的第一个块就自定义完毕了,那么我们仍然可以测试一下(最好是一定要测试一下,确定每个快没有错误之后,减少最终他们组建起来的错误)

in1=keras.Input(shape=(256,256,3))
s1=StemBlock()
s1(in1)

在这里插入图片描述

根据上图我们可以知道这里的理想输出是缩小为原图的1/4,维度数为16,与我这里构造的输出达到了完全一样的结果也就是我这里的结构创造的是正确的。

1.2.2 Gather-and-Expansion Layer的创建

GE,全称Gather-and-Expansion Layer,我个人翻译为特征聚集扩展层,在这里作者提供了他的多种结构

在这里插入图片描述

​ 其中a为是MobileNetv2中提出的移动反向瓶颈卷积,当步长为2的时候,虚线所示内容不存在。b,c都为建议的特征聚集扩展结构我这里采用较为复杂的c结构(多出的层效果应该会更好一些),所以我在这里选择了C结构进行搭建。

​ 那么可以看到这里的结构有一种特殊的卷积DWconv,全称DepthwiseConv2D,这种特殊的卷积层的卷积核个数不是我们人为决定的,是由输入数据的维度数决定的,他的处理数据方式如下图(https://blog.csdn.net/weixin_43937316/article/details/99545506),但是层提供了一个扩展因子参数depth_multiplier(默认情况下输出为1),数据H* w* c输入步长为1,padding方式为’same’的网络中输出为H* w* (c * depth_multiplier)

在这里插入图片描述

那么可以看到在提供的C结构中只由conv,DWConv这两中卷积为主体,所以对于该结构,我也就直接给出我的代码了

#因为在论文给出的结构中卷积层与BN层总是连在一起吗,所以我们为了代码复用性自定义层,控制卷积核大小,步长,和膨胀系数e
class DWConv(layers.Layer):
    def __init__(self,kernel_size,strides,e=1):
        super(DWConv,self).__init__()
        self.dwconv=layers.DepthwiseConv2D(kernel_size=kernel_size,strides=strides,depth_multiplier=e,padding='same')
        #为了防止图片大小尺寸错误,这里设置填充方式为'same
        self.bn=layers.BatchNormalization()
    def call(self,input):
        x=self.dwconv(input)
        return self.bn(x)
 
class GatherExpansion(layers.Layer):
    def __init__(self,units,expansion_ration,layers_name='',strides=2):
        super(GatherExpansion,self).__init__()
        self.conv1=ConvBlock(units=units,kenral_size=3,strides=1)
        self.conv2=ConvBlock(units=units,kenral_size=1,strides=1,use_activation=False)
        self.conv3=ConvBlock(units=units,kenral_size=1,strides=1,use_activation=False)
        self.dwconv1=DWConv(kernel_size=3,
                            strides=strides,
                            e=expansion_ration)
        self.dwconv2=DWConv(kernel_size=3,
                            strides=1,
                            e=1)
        self.dwconv3=DWConv(kernel_size=3,
                            strides=strides,
                            e=1)
        self.relu=layers.ReLU(name=layers_name)                    
    def call(self,input):
        x=self.conv1(input)
        x1=self.dwconv1(x)
        #这里是结构中唯一一次扩展通道将输出特征向高维输出
        x1=self.dwconv2(x1)
        x1=self.conv2(x1)
        x2=self.dwconv3(input)
        x2=self.conv3(x2)
        x3=tf.add(x1,x2)#两层分支最后加和      
        return self.relu(x3)#加和之后relu激活输出
1.2.3 ContextEmbeldingBlock的创建

CE,全称ContextEmbeldingBlock,原文中是这么说的“语义分支需要较大的接受域来捕获高级语义。因此,我们设计了一个具有全局平均池的上下文嵌入块来嵌入全局上下文信息”(因为博主太菜了,所以就直接引用原文QAQ),他的结构如下

在这里插入图片描述

是一个非常简单的结构输入数据经过全局池化(保留维度的全局池化),再经过不改变数据大小的卷积层处理后输出为(1 * 1 * C),与我们的原输入相加(H * W* C),这里其实在相加的时候运用了类似ndarray的广播机制,在相加的时候1* 1* C的数据自动扩展成H *W *C,然后与原数据相加,所以最终输出为H *W *C,代码如下:

class ContextEmbelding(layers.Layer):
    def __init__(self,units):
#这里需要注意我们的数据输入输出的维度数是要一样的,所以我的
        super(ContextEmbelding,self).__init__()
        self.conv1=ConvBlock(units,kenral_size=1,strides=1)
        self.conv2=ConvBlock(units,kenral_size=3,strides=1)
    def call(self,input):
        x=tf.reduce_mean(input,axis=[1,2],keepdims=True)
        #保持维度不变的求平均值
        x=layers.BatchNormalization()(x)
        x=self.conv1(x)
        x1=tf.add(input,x)#相加
        x1=self.conv2(x1)
        return x1

至此组成分割分支的所有组件,我们就全部自定义完了,但是再组装的时候还有个问题是我们需要注意的,在结构图中可以看到,分割分支中间总会输出一些数据,经过segHead,然后去计算loss,这里作者称之为分割头。“为了进一步提高分割精度,我们提出了一种增强训练策略。顾名思义,它类似于火箭助推器:它可以在训练阶段增强特征表示,在推理阶段可以放弃。因此,它在推理阶段增加的计算复杂度很少。”(这里的推理阶段指的是用于验证的时候?)

在这里插入图片描述

也就是在分割分支中,我们插入分割头,使得在模型还未完成的时候便计算loss,然后去应用于模型,所以我在这里先定义分割头的结构,在这里作者给出了它的结构

在这里插入图片描述

可以看到经过两层卷积后最后上采样输出结果去计算loss,那么在这里Ct参数也就是第一个卷积核的单元数,可以控制计算的复杂性,这里我个人因为CityScapes数据集的种类有34种,我倾向于先卷积为64层,然后第二层卷积为34层,最后上采样,为了减少计算量,最后我们直接使用双线性插值上采样(相对于反卷积这样的计算量会减少)。代码如下:

class SegHead(layers.Layer):
    def __init__(self,units,numclasses,size):
        #上采样的倍数由不同数据而改变
        super(SegHead,self).__init__()
        self.conv1=ConvBlock(units=units,kenral_size=3,strides=1)
        self.conv2=ConvBlock(units=numclasses,kenral_size=1,strides=1,use_activation=False)
        self.up=layers.UpSampling2D(size,interpolation='bilinear')
    def call(self,input):
        x1=self.conv1(input)
        x1=self.conv2(x1)
        x1=self.up(x1)
        return x1

1.3 双向引导汇聚层层的创建

在数据分别从细节分支和分割分支后输出我们还要将他聚合作为我们的最后输出,作者给出的结构如下,

在这里插入图片描述

这里细节分支的输出大小为原图的1/8,分割分支的输出为1/32,也就是分割分支的输出大小为细节分支的1/4,可以看到这里结构都非常的明显,分割分支经过处理sigmoid激活输出,然后与经过处理的细节分支相乘,这里要注意一点,这里在最后加和的时候不知道是作者没注意还是什么,最后sum的时候一个大小时另一个大小的1/4直接求和会出错,所以我个人是将右边的输出经过一次上采样之后与左边输出相同大小,在加和所以定义该模块代码如下:

class FeatureFusion(layers.Layer):
    def __init__(self,units=128,numclasses=34):
        super(FeatureFusion,self).__init__()
        self.dwconv1=DWConv(kernel_size=3,strides=1)
        self.conv1=ConvBlock(units=units,kenral_size=3,strides=2,use_activation=False)
        self.conv2=layers.Conv2D(units,kernel_size=1,strides=1,padding='same')
        self.avgpool=layers.AveragePooling2D(pool_size=3,strides=2,padding='same')
        self.conv3=ConvBlock(units=units,kenral_size=3,strides=1,use_activation=False)
        self.dwconv2=DWConv(kernel_size=3,strides=1)
        self.up1=layers.UpSampling2D(size=4,interpolation='bilinear')
        self.up2=layers.UpSampling2D(size=4,interpolation='bilinear')
        self.conv4=ConvBlock(units=units,kenral_size=1,strides=1,use_activation=False)
        self.conv5=ConvBlock(units=numclasses,kenral_size=3,strides=1,use_activation=False)
    def call(self,DB_input,SB_input):
        x1=self.dwconv1(DB_input)
        x1=self.conv2(x1)
        x2=self.conv1(DB_input)
        x2=self.avgpool(x2)
        x3=self.conv3(SB_input)
        x3=self.up1(x3)
        x3=tf.nn.sigmoid(x3)
        x4=self.dwconv2(SB_input)
        x4=self.conv4(x4)
        x4=tf.nn.sigmoid(x4)
        x=tf.multiply(x1,x3)
        y=tf.multiply(x2,x4)
        y=self.up2(y)
        out=tf.add(x,y)
        out=self.conv5(out)
        return out

1.4 整体模型的创建

在定义完组成整个模型的所有组件之后,我们接下来就开始将整个模型拼装在一起

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xE0lpgMN-1613790565150)(C:\Users\admin\Desktop\suanfa\博客BiseNetv2\2.png)]

这里我们直接按照如图给出的分支来构造整个网络,其中k是卷积核大小,c是输出维度,e为拥有DWConv网络的膨胀系数,s为步长,r为重复次数,这里由于我们要获得中间的多个输出,我这里打算使用函数式API来写,那我们就依葫芦画瓢,照着表格写网络

input1=layers.Input(shape=(1024,2048,3))
#CityScapes的数据大小为(1024,2048,3)
s1=StemBlock()
x1=s1(input1)#首先经过StemBlock
ge1=GatherExpansion(units=32,expansion_ration=6,layers_name='ge1',strides=2)
x2=ge1(x1)
ge2=GatherExpansion(units=32,expansion_ration=6,layers_name='ge2',strides=1)
x2=ge2(x2)#这里的x2是第二个需要经过分割头的数据
ge3=GatherExpansion(units=64,expansion_ration=6,layers_name='ge3',strides=2)
ge4=GatherExpansion(units=64,expansion_ration=6,layers_name='ge4',strides=1)
x3=ge3(x2)
x3=ge4(x3)
ge5=GatherExpansion(units=128,expansion_ration=6,layers_name='ge5',strides=2)
ge6=GatherExpansion(units=128,expansion_ration=6,layers_name='ge6',strides=1)
ge7=GatherExpansion(units=128,expansion_ration=6,layers_name='ge7',strides=1)
ge8=GatherExpansion(units=128,expansion_ration=6,layers_name='ge8',strides=1)#这里按照论文所说聚集扩展层重复三次

x4=ge5(x3)
x4=ge6(x4)
x4=ge7(x4)
x4=ge8(x4)
x4

在这里插入图片描述

这里可以清楚的看到,最终我们得到了需要用于输入分割头计算损失的四个数据,那么接下来我们就可以将所有零件拼装起来,作为我们的整体模型

class BiSeNetV2(keras.Model):
    def __init__(self,numclasses=34):
        super(BiSeNetV2,self).__init__()
        self._DetailBranch=DetailBranch()
        self.model1=model1=keras.models.Model(inputs=input1,outputs=x4)#利用函数式API创建模型,在上面的四个数据我们只需要x4作为我的最终输入
        self.FeatureFusion=FeatureFusion()
        self.contex=ContextEmbelding(128)
        self.up1=SegHead(units=64,numclasses=34,size=8)
    def call(self,input):
        DB_input=self._DetailBranch(input)#细节分支的输入
        x4=self.model1(input)
        SB_input=self.contex(x4)#最后经过上下文嵌入快
        result=self.FeatureFusion(DB_input,SB_input)
        result=self.up1(result)#z最后输出经过分割头作为我们的输出
        return result

至此,我们的整个模型就创造完成了(由于整个论文有给出非常详细的结构图,所以我在对于较为简单的结构注释上就没有过多注释)

2.数据预处理

本次使用的数据集是语义分割中经常使用的数据集,在我之前搭建BiseNet的博客(BiseNet学习:利用tensorflow2搭建BiseNet并训练完成语义分割任务_theworld666的博客-CSDN博客)中我介绍过,但是忘了写数据预处理部分,所以这里我就介绍一下我个人对于CityScapes的数据处理的部分所采用的操作(也算是我个人对于自己的备忘录)

首先我们明确目标,要读入数据的是一张原图和标注好的分割图,那么如果要读取图片就需要先获得图片的地址,所以我们先获得所有图片的地址

import glob
all_image_path=glob.glob('../input/cityscapes/Cityspaces/images/train/*/*.png')
#该数据集训练集的所有数据在train文件夹下的所有图片
all_label_path=glob.glob('../input/cityscapes/Cityspaces/gtFine/train/*/*_gtFine_labelIds.png')
#训练接对应的标签数据就是后缀为_gtFine_labelIds的图片

读取完所有图片了之后这里出现了一个问题,那就是我们如何保持图片与标签保持不变呢,这里其实很简单,我们经过排序之后,因为图片地址后缀一样所以对应的图片标签,就一定会在一个位置里所以我们编写代码,排序,然后为了增强数据在使用相同的种子,进行随机乱序,最终查看得到的结果,数据与标签是否一致

import numpy as np
all_label_path.sort()
all_image_path.sort()
index=np.random.permutation(len(all_image_path))
all_image_path=np.array(all_image_path)[index]
all_label_path=np.array(all_label_path)[index]
all_label_path[510:515],all_image_path[510:515]

在这里插入图片描述

可以看到我们随机得到的截取结果,数据和标签的名称都是一模一样的所以,我们就完成了整个数据的搭建,这里我们可以查看一下数据与标签

import matplotlib.pyplot as plt
def read_png_image(path):
    img=tf.io.read_file(path)
    img=tf.image.decode_png(img,channels=3)
    return img
def  read_png_label(path):
    img=tf.io.read_file(path)
    img=tf.io.decode_png(img,channels=1)
    return img   #定义读取图片和标签,标签为灰度图
img_1=read_png_image(all_image_path[0])
label_1=read_png_label(all_label_path[0])
plt.subplot(1,2,1)
plt.imshow(img_1.numpy())
plt.subplot(1,2,2)
plt.imshow(label_1.numpy())

在这里插入图片描述

可以看到我们成功的读取了图片和标签,那么接下来我们就可以开始创建输入模型的管道

@tf.function
def normal_data(img,label):
    img=tf.cast(img,tf.float32)
    img=img/255.0
    img=img*2-1#将整张图片的值规范在[-1,1]之间
    label=tf.cast(label,tf.float32)
    return img,label
def path_to_data_train(img_path,label_path):
    img=read_png_image(img_path)
    label=read_png_label(label_path)
    img=tf.image.resize(img,(1024,2048))
    label=tf.image.resize(label,(1024,2048))
    if tf.random.uniform(())>0.5:#随机左右翻转图像,增强数据
        img=tf.image.flip_left_right(img)
        label=tf.image.flip_left_right(label)
    return normal_data(img,label)
def path_to_data_test(img_path,label_path):#这里是测试数据的读取
    img=read_png_image(img_path)
    label=read_png_label(label_path)
    img=tf.image.resize(img,(1024,2048))
    label=tf.image.resize(label,(1024,2048))
    return normal_data(img,label)
BATCH_SIZE=2#这个批次大小是在是设备扛不住最终设定为2
BUFFER_SIZE=100
train_count=2975
val_count=500
step_per_epoch=train_count//BATCH_SIZE
val_step=val_count//BATCH_SIZE
auto=tf.data.experimental.AUTOTUNE#这个参数是加强CPU读取图片能力的

确定好所有参数后,我们开始划分训练集与测试集,并对训练集进行处理,设置各项参数

dataset_train=tf.data.Dataset.from_tensor_slices((all_image_path,all_label_path))
dataset_val=tf.data.Dataset.from_tensor_slices((img_val,label_val))
dataset_train=dataset_train.map(path_to_data_train,num_parallel_calls=auto)#使用map函数可以使用对应函数对于数据集中的所有数据按照所示函数进行转换
dataset_val=dataset_val.map(path_to_data_test,num_parallel_calls=auto)#验证集的读取方法与测试集一样,所以我就没有再说明
dataset_val,dataset_train

3.模型训练与结果评估

搭建完了模型,处理完了数据,那么万事具备,我们接下来自定义训练步骤就行了(由于使用了自定义模型,使用model.fit可能会报错,所以我们个人自定义模型)就可以了,那么我们先定义各个自定义训练组件如下

#语义分割有一个重要评估标准IOU,这里我们也需要自定义
class MeanIOU(keras.metrics.MeanIoU):
    def __call__(self,y_true,y_pred):
        y_pred=tf.argmax(y_pred,axis=-1)
        return super().__call__(y_true,y_pred)
optimizer=keras.optimizers.SGD(learning_rate=0.01,momentum=0.9)
#按照论文所说,这里我们使用SGD优化器
loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True)#指定多分类损失,并且由于最后没有激活所以指定from_logits为True
train_loss=keras.metrics.Mean(name='train_loss')
train_acc=keras.metrics.SparseCategoricalAccuracy(name='train_accuracy')
train_iou=MeanIOU(34,name='train_iou')
test_loss=keras.metrics.Mean(name='test_loss')
test_acc=keras.metrics.SparseCategoricalAccuracy(name='test_accuracy')
test_iou=MeanIOU(34,name='test_iou')
#指定用于验证的损失,准确率,iou

还记得我们之前说过的分割头结构,提前计算损失并应用于模型这里我们定义他如下

def rocket (inputs,units,size,numclasses,x):
    model=keras.models.Model(inputs=input1,outputs=x)
    with tf.GradientTape() as t:
        pred=model(inputs)
        pred=SegHead(units,numclasses,size)(pred)
        loss1=loss(labels,pred)#计算损失
    gradies=t.gradient(loss1,net.trainable_variables)
    #求解梯度
    optimizer.apply_gradients(zip(gradies,net.trainable_variables))#应用梯度

然后在训练步骤中,我们这样采用


def train_step(images,labels):
    rocket(images,16,4,numclasses=34,x=x1)
    rocket(images,32,8,numclasses=34,x=x2)
    rocket(images,64,16,numclasses=34,x=x3)
    rocket(images,128,32,numclasses=34,x=x4)#增加四层分割头结构
    with tf.GradientTape() as t:
        pred=net(images)
        loss_step=loss(labels,pred)
    gradies=t.gradient(loss_step,net.trainable_variables)
    #求解梯度
    optimizer.apply_gradients(zip(gradies,net.trainable_variables))#将梯度应用于优化器从而让模型的可训练参数改变
    train_loss(loss_step)
    train_acc(labels,pred)
    train_iou(labels,pred)

那么开始我们的训练


def test_step(images,labels):
    pred=net(images)
    loss_step=loss(labels,pred)
    test_loss(loss_step)
    test_acc(labels,pred)
    test_iou(labels,pred)
Epoch=20#先训练20次
for epoch in range(Epoch):
    train_loss.reset_states()#重置每个参数的状态
    train_acc.reset_states()
    train_iou.reset_states()
    test_acc.reset_states()
    test_loss.reset_states()
    train_iou.reset_states()
    for images,labels in dataset_train:
        train_step(images,labels)
        print('-',end='')#查看训练没批次是否完成
    print('>')
    for img_test,label_test in dataset_val:
        test_step(img_test,label_test)
    template = 'Epoch {:.3f}, Loss: {:.3f}, Accuracy: {:.3f}, \
                IOU: {:.3f}, Test Loss: {:.3f}, \
                Test Accuracy: {:.3f}, Test IOU: {:.3f}'
    print (template.format(epoch+1,
                           train_loss.result(),
                           train_acc.result()*100,
                           train_iou.result(),
                           test_loss.result(),
                           test_acc.result()*100,
                           test_iou.result() 
                           ))#输出我们的评价结果

我们可以来看一下训练结果,在经过多次训练后,达到效果如下:

Epoch 17.000, Loss: 0.608, Accuracy: 82.627, IOU: 0.251, Test Loss: 0.632, Test Accuracy: 81.939, Test IOU: 0.195

Epoch 18.000, Loss: 0.594, Accuracy: 82.996, IOU: 0.256, Test Loss: 0.631, Test Accuracy: 82.225, Test IOU: 0.197

可以看到在第十七次到第十八次的时候我们的模型仍有非常明显的上升趋势(但是机器扛不住了,我是在KAGGLE上训练的,已经达到KAGGLE能允许离线的最长时间了),我个人认为如果使用预训练神经网络应该能在几次训练中迅速达到非常高的正确率(因为使用了提前训练好的权重),但是这里由于采用了我们自己搭建的架构,权重是随机初始化的,所以我的正确率在十几次训练的时候达到82.996(十几次在训练中也算少了吧,只是由于本人没设备就只能训练这几次),但是自己搭建的话上限应该会比预训练神经网络为架构的模型高。有兴趣的朋友可以自己搭建模型训练一下(我看看什么时候优化下模型和训练步骤再把博客修改一下

︿( ̄︶ ̄)︿)。

结语

在本篇博客中,博主按照论文和源码完成了BiseNetV2的所有网络组件的搭建,并搭建了网络,完成了对CityScapes数据集的预处理,并最终训练评估了整体模型。由于博主个人水平有限,所以出现错误在所难免,如果有任何建议或者疑问欢迎在评论区交流。

  • 13
    点赞
  • 92
    收藏
    觉得还不错? 一键收藏
  • 32
    评论
好的,以下是一些常用的语义分割数据集以及FCN网络搭建过程。 ## 常用的语义分割数据集 1. PASCAL VOC:该数据集包含了20个对象类别,涵盖了大部分常见的对象,如人、车、动物等。其中训练集和验证集共包含1464张图像,测试集包含1456张图像。 2. COCO:该数据集包含了80个对象类别,图像的数量超过了30万张。COCO数据集的特点是对象类别多、图像分辨率高,对算法的要求也比较高。 3. Cityscapes:该数据集是针对城市场景的语义分割数据集,包含了19个对象类别,提供了5000张用于训练的图像和500张用于测试的图像。 4. ADE20K:该数据集包含了超过20000张图像,涵盖了200个对象类别。ADE20K数据集的特点是场景复杂、对象类别多,对算法的要求比较高。 ## 搭建FCN网络 以下是搭建FCN网络的步骤: 1. 导入必要的库和模块: ```python import tensorflow as tf from tensorflow.keras.models import Model from tensorflow.keras.layers import Input, Conv2D, MaxPooling2D, UpSampling2D ``` 2. 定义输入层: ```python input_layer = Input(shape=(height, width, channels)) ``` 3. 定义卷积层和池化层: ```python conv1 = Conv2D(64, 3, activation='relu', padding='same')(input_layer) pool1 = MaxPooling2D(pool_size=(2, 2))(conv1) conv2 = Conv2D(128, 3, activation='relu', padding='same')(pool1) pool2 = MaxPooling2D(pool_size=(2, 2))(conv2) conv3 = Conv2D(256, 3, activation='relu', padding='same')(pool2) pool3 = MaxPooling2D(pool_size=(2, 2))(conv3) ``` 4. 定义上采样层和反卷积层: ```python up4 = UpSampling2D(size=(2, 2))(pool3) conv4 = Conv2D(256, 3, activation='relu', padding='same')(up4) up5 = UpSampling2D(size=(2, 2))(conv4) conv5 = Conv2D(128, 3, activation='relu', padding='same')(up5) up6 = UpSampling2D(size=(2, 2))(conv5) conv6 = Conv2D(64, 3, activation='relu', padding='same')(up6) ``` 5. 定义输出层: ```python output_layer = Conv2D(num_classes, 1, activation='softmax', padding='same')(conv6) ``` 6. 定义模型并编译: ```python model = Model(inputs=input_layer, outputs=output_layer) model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy']) ``` 以上是一个简单的FCN网络搭建过程,您可以根据自己的需求进行修改和优化。同时,您可以使用以上提到的语义分割数据集进行训练和测试。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值