BiseNet学习:利用tensorflow2搭建BiseNet并训练完成语义分割任务

2024-03-29 13:48

本文主要是介绍BiseNet学习:利用tensorflow2搭建BiseNet并训练完成语义分割任务,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

BiseNet学习:利用tensorflow2从头搭建BiseNet并训练完成语义分割任务

文章目录

  • BiseNet学习:利用tensorflow2从头搭建BiseNet并训练完成语义分割任务
    • 简介
    • 1 数据集的简介
    • BiseNet网络搭建
    • 1)BiseNet网络各个模块简介
      • 1.1 Spatial path搭建
      • 1.2 Context path的搭建
      • 1.3特征融合模块
    • 2.模型的初始化,训练以及测试
      • 2.2训练结果的评价与结果可视化
      • 2.3效果可视化
    • 3.结语

简介

BiseNet是于旷视于2018提出的轻量级实时语义分割网络,之所以今天谈到他是几个月前的博主在一项调研语义分割工作中,找到了它的改进版本BiseNetV2。但是最终无论如何都没有让程序跑动起来(这里可能是当时我懂得太少了,在各项如何修改错误方面完全不知道,最终留下遗憾),现在经过最近的学习,博主自认为应该可以来自己搭建网络,这样需要什么环境就可以靠自己来定,那么我就先从它的原型BiseNet开始学习并搭建。

1 数据集的简介

CityScapes数据集(https://www.cityscapes-dataset.com/),记录了欧洲各大城市的马路上数据,车载相机拍摄得照片,所以你可以在多个图片上看见有个圆圆的车标,

在这里插入图片描述

他的所有图片的尺寸都是 1024*2048,测试集共有2975张图片,验证集有500张图片,他已经成为了各个语义分割网络结构比拼性能的地方,甚至有排行榜

在这里插入图片描述

BiseNet网络搭建

阅读论文(https://link.zhihu.com/?target=https%3A//arxiv.org/abs/1808.00897)的时候,作者认为在当前大部分语义分割网络为了提升速度(语义分割最终得到的是一张分割图,区别于之前图像分类,定位,预测等任务他们其实都只是输出一个分类值,而输出一张分割图无疑会使计算量大大增加)都大部分会通过剪裁(crop)或 resize 来限定输入大小或不断减少网络通道数,以降低计算复杂度。尽管这种方法简单而有效,但作者认为这会损失某些细节,让预测大打折扣。所以在BiseNet提出的结构中最后我们输出的会是一张和原图一模一样大小的分割图(虽然这样其实也会极大加大运行内存),但我们的精确度却会达到较高的水准

在这里插入图片描述

在论文中说明使用Xception在验证集能达到71.4,这里我就采用Xception来作为网络的BaseModel(ResNet,博主了解还不够好,在学了在学了),那么接下来我们就废话少说开始我们的网络搭建。

1)BiseNet网络各个模块简介

在这里插入图片描述

论文这里用了一张非常鲜明的图来介绍了我们的网络架构,可以看到整个模型分为了Spatial path,和contextpath两条分支,其中我们先来介绍Spatial path。

1.1 Spatial path搭建

这里论文里说明,“我们提出了一种空间路径来保留原始输入图像的空间大小并编码丰富的空间信息。空间路径中包含三层。每一层包括一个stride = 2的卷积,接着是BatchNormalization和ReLU激活(也有人说可以直接激活再BatchNormalization)。因此经过这三层卷积,该路径提取的输出特征映射是原始图像的1/8。由于地物地图的空间尺寸较大,它能够编码丰富的空间信息。”

那我们就简单了这里直接利用自定义层开始定义,由于这里每一层都是卷积+批标准化层加ReLU激活,这里我们可以直接定义一个组合层

class ConvBlock(layers.Layer):def __init__(self,out_channels,kernel_size=3,stride=2,padding='same'):super(ConvBlock,self).__init__()self.conv1 = layers.Conv2D(out_channels,kernel_size=kernel_size,strides=stride,padding='same')self.bn = layers.BatchNormalization()def call(self, input):x = self.conv1(input)x = self.bn(x)x=tf.nn.relu(x)return x

那么对于Spatial path卷积层的卷积核数可以自己定义,我这里采用64-128-256形式不断增加,最终完成原图输入H * w *3——》H/8 * W/8 * 256

1.2 Context path的搭建

​ 这里原文作者说明Context path这里可以直接采用Xception等预训练神经网络来作为特征提取所以我们这里也直接采用Xception网络


xception=keras.applications.Xception(include_top=False,weights='imagenet',input_shape=(1024,2048,3))

​ 那么根据上图网络结构,我们分别需要对下采样到原图大小的1/16,1/32开始操作,那么我们先来查看xception网络结构

在这里插入图片描述

可以看到Xception的block13_sepconv2_bn , block14_sepconv2_act层就是我们要找的层(即原图的1/16,1/32),那么如何获取中间层的输出呢,我们接下来采用该方法来完成

layers_output=[xception.get_layer(layer_name).output for layer_name in layers_names]
multi_out_model=keras.models.Model(inputs=xception.input,outputs=layers_output)

那么获取了中间层的输出了,论文中说我们还有一个注意力增加(Attention Refinment Module)模块,我们通过该图得知

在这里插入图片描述

输入的张量分叉为两条,一条经过全局池化,卷积核大小为1的卷积,批标准化层,sigmoid激活后,最后与原输入相乘输出我们的最终结果。但是这里注意这里的全局池化并不是keras中内置的全局平均池化层,因为之后还要经过一个二维卷积层,那么我们的维度只能是四维,但原文在这里也没说太清楚,于是我去找了别人提供的代码(使用torch写的),发现这里的所谓全局池化其实是在最后channels,和长度这两维度求平均,所以我们的代码如此编写

class AttentionRefinementModule(layers.Layer):def __init__(self,out_channels):super(AttentionRefinementModule,self).__init__()self.conv1=layers.Conv2D(out_channels,kernel_size=1,padding='same')self.bn=layers.BatchNormalization()def call(self, input):x=tf.reduce_mean(input,axis=3,keepdims=True)x=tf.reduce_mean(x,axis=2,keepdims=True)#上面两行为求全局平均池化层x=self.conv1(x)x=self.bn(x)x=tf.nn.sigmoid(x)return tf.multiply(x,input)

这里要注意一点我们在写模型的时候最好是能知道输出的维度方便我们处理,这里我们推导一下输出的数据假设输入为(None, 32, 64, 2048),那么我们经过前面的全局池化后变为(None, 32, 1, 1),然后卷积激活不改变大小最后原输入(None, 32, 64, 2048)相乘最终结果为(None, 32, 64, 2048),也就是我们最终输出与输入保持了一致。

那么我们看到接下来下采样16倍和32倍和的数据还要在经过ARM和一系列的融合,这里我在代码里解释

down_16,down_32=self.ml(input)
#这里是我们刚才定义的多输出模型,他输出下采样16倍和32倍后的输出output_arm16=self.arm16(down_16)
output_arm32=self.arm32(down_32)
#将他们经过多输出模型注意,这里输出后他们的形状与原来一模一样
tail=tf.reduce_mean(down_32,axis=3,keepdims=True)
tail=tf.reduce_mean(down_32,axis=2,keepdims=True)
output_tail=tf.multiply(output_arm32,tail)

然后我们接下来要将这边的输出与空间路径的输出连接(concat)在一起,那么经过空间路径输出为原图的1/8,所以我们这里还要将原图这里的1/16,1/32上采样原来的1/8然后再concate连接到一起

self.up1=layers.UpSampling2D(2,interpolation='bilinear')     self.up2=layers.UpSampling2D(4,interpolation='bilinear')
#注这里是将自定义模型中的代码抽出来展示,上采样既可以采取反卷积,也可以直接上采样,但考虑到这是轻量级神经网络,我们这里直接上采样减少计算量
output_1=self.up1(output_arm16)
output_tail=self.up2(output_tail)
output_cp=tf.concat([output_1,output_tail],axis=-1)

最终ContextPath输出的维度是(None,128,256,3072)

那么接下来我们介绍用于融合空间路径和Contextpath两个输出的模块的特征融合模块。

1.3特征融合模块

在这里插入图片描述

经过我们之前的叙述,他的输入是(None,128,256,256)(None,128,256,3072),他们被连接后的最终维度为(None,128,256,3328),然后接下来卷积我们这里由于是最后输出了,我们将卷积核个数变为种类数34,然后分支的与之前的分支结构相同这里不再过多赘述上代码

class FeatureFusionModule(layers.Layer):def __init__(self,num_classes):super(FeatureFusionModule,self).__init__()self.convblock = ConvBlock(out_channels=num_classes,kernel_size=3,stride=1)self.conv1 = layers.Conv2D(34,(1,1))self.conv2 = layers.Conv2D(34,(1,1))def call(self, input_1, input_2):x=tf.concat([input_1,input_2],axis=-1)feature = self.convblock(x)x=tf.reduce_mean(feature,axis=3,keepdims=True)x=tf.reduce_mean(x,axis=2,keepdims=True)x=self.conv1(x)x=tf.nn.relu(x)x=self.conv2(x)x=tf.nn.sigmoid(x)x = tf.multiply(feature, x)x = tf.add(x, feature)return x

这样我们就将模型的每一步都具体分析并用代码展示完了,最后构成整个网络的结构代码如下

class ConvBlock(layers.Layer):def __init__(self,out_channels,kernel_size=3,stride=2,padding='same'):super(ConvBlock,self).__init__()self.conv1 = layers.Conv2D(out_channels,kernel_size=kernel_size,strides=stride,padding='same')self.bn = layers.BatchNormalization()def call(self, input):x = self.conv1(input)x = self.bn(x)x=tf.nn.relu(x)return xclass Spatial_path(layers.Layer):def __init__(self):super(Spatial_path,self).__init__()self.convblock1 = ConvBlock(out_channels=64)self.convblock2 = ConvBlock(out_channels=128)self.convblock3 = ConvBlock(out_channels=256)def call(self, input):x = self.convblock1(input)x = self.convblock2(x)x = self.convblock3(x)return x
class AttentionRefinementModule(layers.Layer):def __init__(self,out_channels):super(AttentionRefinementModule,self).__init__()self.conv1=layers.Conv2D(out_channels,kernel_size=1,padding='same')self.bn=layers.BatchNormalization()def call(self, input):x=tf.reduce_mean(input,axis=3,keepdims=True)x=tf.reduce_mean(x,axis=2,keepdims=True)x=self.conv1(x)x=self.bn(x)x=tf.nn.sigmoid(x)return tf.multiply(x,input)
class FeatureFusionModule(layers.Layer):def __init__(self,num_classes):super(FeatureFusionModule,self).__init__()self.convblock = ConvBlock(out_channels=num_classes,kernel_size=3,stride=1)self.conv1 = layers.Conv2D(34,(1,1))self.conv2 = layers.Conv2D(34,(1,1))def call(self, input_1, input_2):x=tf.concat([input_1,input_2],axis=-1)feature = self.convblock(x)x=tf.reduce_mean(feature,axis=3,keepdims=True)x=tf.reduce_mean(x,axis=2,keepdims=True)x=self.conv1(x)x=tf.nn.relu(x)x=self.conv2(x)x=tf.nn.sigmoid(x)x = tf.multiply(feature, x)x = tf.add(x, feature)return x
class BiseNet(keras.Model):def __init__(self,numclasses=34):super(BiseNet,self).__init__()self.sp=Spatial_path()self.arm16=AttentionRefinementModule(1024)self.arm32=AttentionRefinementModule(2048)self.up1=layers.UpSampling2D(2,interpolation='bilinear')self.up2=layers.UpSampling2D(4,interpolation='bilinear')self.ffm=FeatureFusionModule(34)self.ml=keras.models.Model(inputs=xception.input,outputs=layers_output)self.conv=layers.Conv2D(34,(1,1),padding='same')def call(self,input):x1=self.sp(input)down_16,down_32=self.ml(input)output_arm16=self.arm16(down_16)output_arm32=self.arm32(down_32)tail=tf.reduce_mean(down_32,axis=3,keepdims=True)tail=tf.reduce_mean(down_32,axis=2,keepdims=True)output_tail=tf.multiply(output_arm32,tail)output_1=self.up1(output_arm16)output_tail=self.up2(output_tail)output_cp=tf.concat([output_1,output_tail],axis=-1)result=self.ffm(x1,output_cp)result=layers.UpSampling2D(8,interpolation='bilinear')(result)result=self.conv(result)return result

2.模型的初始化,训练以及测试

那么接下来,我们开始对于模型的使用,对于自定义模型,我们需要知道,自己打的代码是非常容易出错的,那么我们在训练的时候加载数据又非常耗时间,所以我们如果在没有确认自定义模型无错误就去训练,代价是非常大的,所以我们要先去修改完错误,那么怎么不读取数据就完成自定义模型初始化呢,很简单

net=BiseNet()
net(xception.input)
#xception.input数据类型是下面这个
<KerasTensor: shape=(None, 1024, 2048, 3) dtype=float32 (created by layer 'input_1')>

我们可以利用这个方法来完成模型内部各项数据的初始化,同时各项错误也会显示出来,以便我们做出修改。

那么接下来我们自定义各项训练步骤

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)
keras.optimizers.Adam(0.0001)
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,损失函数,准确率,那么接下你我们自定义训练步骤

@tf.function
def train_step(images,labels):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)
@tf.function
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)

这里我们在每一批次的训练函数上加tf.function,tf2会自动将该运算转化为图运算从而加快运算速度。最后我们就直接开始训练,(这里要说一句,由于我们的运算最终得到的是原图大小所以最终导致Batch_size在kaggle提供的GPU上都只能为1,。。。并且一次训练接近一小时,不竟让我怀疑他到底是不是轻量级网络。。还是我经历的太少了。。)

Epoch=1
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='')#标志训练完一个batchprint('>')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() ))

2.2训练结果的评价与结果可视化

在这里插入图片描述

可以看到我在一次训练就在测试集达到89.703%的准确率,IOU达到了0.405,这里相比于博主之前训练的Unet
在这里插入图片描述

一次训练正确率只在56%,IOU也只有0.062的可以看到正确率和IOU都有非常大的提高,这里我总共训练了10次,每次都是接近一小时的训练时间,最终我们在测试集达到了一个很高的准确率95.014%和一个较好的IOU 71.6%

在这里插入图片描述

对比论文提供的效果

在这里插入图片描述

可以看到我们在测试集取得的效果(这里我采用的是Xception,所以我们对比看这一行)距离作者在测试集上我们取得的效果已经是非常接近了,不足的是这里我们的模型过拟合,在测试集上的IOU也只有0.483,但是由于设备有限(设置的batch大小只是1,并且训练十遍机器就停了),所以我也没有继续训练下去,(也是我菜不知道该在哪里,添加网络抑制过拟合(数据增强的话我就加了一个随机左右翻转,毕竟他要原图大小,不知道怎么裁剪),有会的朋友可以评论区交流),那么接下来展示效果可视化,我在这里是多次断点训练在训练的中间采集效果

2.3效果可视化

1.Epoch1: Loss: 0.466, Accuracy: 87.266, IOU: 0.361, Test Loss: 0.378, Test Accuracy: 88.825, Test IOU: 0.412

(以下测试图片从左往右是原图,预测值,真值)

在这里插入图片描述

2.Epoch3 Loss: 0.279, Accuracy: 91.349, IOU: 0.492,

​ Test Loss: 0.309, Test Accuracy: 90.371, Test IOU: 0.454

在这里插入图片描述

在这里插入图片描述

3.Epoch5 2.000, Loss: 0.223, Accuracy: 92.818, IOU: 0.572,

​ Test Loss: 0.315, Test Accuracy: 91.316, Test IOU: 0.459

在这里插入图片描述

4 Epoch7, Loss: 0.189, Accuracy: 93.801, IOU: 0.637,

Test Loss: 0.307,Test Accuracy: 91.440, Test IOU: 0.472

在这里插入图片描述

在这里插入图片描述

3.结语

博主在这篇博客里完成了对于BiseNet语义分割网络的从头搭建,并且配置了网络各项优化器损失函数,对于网络训练后的效果进行了评估,总体上在测试集上达到了非常高的IOU与论文较为接近,但在验证集上欠拟合,也是博主没有解决的问题。然后对于训练各项结果,最终我进行了效果可视化,将每阶段的效果逐一呈现,可以看到是整个效果模型输出的预测图整体上效果是在不断加强的,关于本篇博客有任何建议或者问题,可以评论区交流,多谢!

这篇关于BiseNet学习:利用tensorflow2搭建BiseNet并训练完成语义分割任务的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


原文地址:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.chinasem.cn/article/858764

相关文章

使用Python实现快速搭建本地HTTP服务器

《使用Python实现快速搭建本地HTTP服务器》:本文主要介绍如何使用Python快速搭建本地HTTP服务器,轻松实现一键HTTP文件共享,同时结合二维码技术,让访问更简单,感兴趣的小伙伴可以了... 目录1. 概述2. 快速搭建 HTTP 文件共享服务2.1 核心思路2.2 代码实现2.3 代码解读3.

MySQL双主搭建+keepalived高可用的实现

《MySQL双主搭建+keepalived高可用的实现》本文主要介绍了MySQL双主搭建+keepalived高可用的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,... 目录一、测试环境准备二、主从搭建1.创建复制用户2.创建复制关系3.开启复制,确认复制是否成功4.同

Spring定时任务只执行一次的原因分析与解决方案

《Spring定时任务只执行一次的原因分析与解决方案》在使用Spring的@Scheduled定时任务时,你是否遇到过任务只执行一次,后续不再触发的情况?这种情况可能由多种原因导致,如未启用调度、线程... 目录1. 问题背景2. Spring定时任务的基本用法3. 为什么定时任务只执行一次?3.1 未启用

SpringBoot使用OkHttp完成高效网络请求详解

《SpringBoot使用OkHttp完成高效网络请求详解》OkHttp是一个高效的HTTP客户端,支持同步和异步请求,且具备自动处理cookie、缓存和连接池等高级功能,下面我们来看看SpringB... 目录一、OkHttp 简介二、在 Spring Boot 中集成 OkHttp三、封装 OkHttp

如何使用Python实现一个简单的window任务管理器

《如何使用Python实现一个简单的window任务管理器》这篇文章主要为大家详细介绍了如何使用Python实现一个简单的window任务管理器,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起... 任务管理器效果图完整代码import tkinter as tkfrom tkinter i

C++字符串提取和分割的多种方法

《C++字符串提取和分割的多种方法》在C++编程中,字符串处理是一个常见的任务,尤其是在需要从字符串中提取特定数据时,本文将详细探讨如何使用C++标准库中的工具来提取和分割字符串,并分析不同方法的适用... 目录1. 字符串提取的基本方法1.1 使用 std::istringstream 和 >> 操作符示

Spring Boot 集成 Quartz 使用Cron 表达式实现定时任务

《SpringBoot集成Quartz使用Cron表达式实现定时任务》本文介绍了如何在SpringBoot项目中集成Quartz并使用Cron表达式进行任务调度,通过添加Quartz依赖、创... 目录前言1. 添加 Quartz 依赖2. 创建 Quartz 任务3. 配置 Quartz 任务调度4. 启

Java使用多线程处理未知任务数的方案介绍

《Java使用多线程处理未知任务数的方案介绍》这篇文章主要为大家详细介绍了Java如何使用多线程实现处理未知任务数,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 知道任务个数,你可以定义好线程数规则,生成线程数去跑代码说明:1.虚拟线程池:使用 Executors.newVir

Spring Boot中定时任务Cron表达式的终极指南最佳实践记录

《SpringBoot中定时任务Cron表达式的终极指南最佳实践记录》本文详细介绍了SpringBoot中定时任务的实现方法,特别是Cron表达式的使用技巧和高级用法,从基础语法到复杂场景,从快速启... 目录一、Cron表达式基础1.1 Cron表达式结构1.2 核心语法规则二、Spring Boot中定

Java进阶学习之如何开启远程调式

《Java进阶学习之如何开启远程调式》Java开发中的远程调试是一项至关重要的技能,特别是在处理生产环境的问题或者协作开发时,:本文主要介绍Java进阶学习之如何开启远程调式的相关资料,需要的朋友... 目录概述Java远程调试的开启与底层原理开启Java远程调试底层原理JVM参数总结&nbsMbKKXJx