憨批的语义分割重制版|憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台


憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台

  • 注意事项
  • 学习前言
  • 什么是DeeplabV3+模型
  • 代码下载
  • DeeplabV3+实现思路
    • 一、预测部分
      • 1、主干网络介绍
      • 2、加强特征提取结构
      • 3、利用特征获得预测结果
    • 二、训练部分
      • 1、训练文件详解
      • 2、LOSS解析
  • 训练自己的DeeplabV3+模型
    • 一、数据集的准备
    • 二、数据集的处理
    • 三、开始网络训练
    • 四、训练结果预测

注意事项 这是重新构建了的DeeplabV3+语义分割网络,主要是文件框架上的构建,还有代码的实现,和之前的语义分割网络相比,更加完整也更清晰一些。建议还是学习这个版本的DeeplabV3+。
学习前言 这是Pytorch版本的!
憨批的语义分割重制版|憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台
文章图片

什么是DeeplabV3+模型 DeeplabV3+被认为是语义分割的新高峰,因为这个模型的效果非常好。
DeeplabV3+主要在模型的架构上作文章,引入了可任意控制编码器提取特征的分辨率,通过空洞卷积平衡精度和耗时。
憨批的语义分割重制版|憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台
文章图片

DeeplabV3+在Encoder部分引入了大量的空洞卷积,在不损失信息的情况下,加大了感受野,让每个卷积输出都包含较大范围的信息。如下就是空洞卷积的一个示意图,所谓空洞就是特征点提取的时候会跨像素。憨批的语义分割重制版|憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台
文章图片

代码下载 Github源码下载地址为:
https://github.com/bubbliiiing/deeplabv3-plus-pytorch
复制该路径到地址栏跳转。
DeeplabV3+实现思路 一、预测部分 1、主干网络介绍
憨批的语义分割重制版|憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台
文章图片

DeeplabV3+在论文中采用的是Xception系列作为主干特征提取网络,本博客会给大家提供两个主干网络,分别是Xception和mobilenetv2。
但是由于算力限制(我没有什么卡),为了方便博客的进行,本文以mobilenetv2为例,给大家进行解析。
MobileNet模型是Google针对手机等嵌入式设备提出的一种轻量级的深层神经网络。
MobileNetV2是MobileNet的升级版,它具有一个非常重要的特点就是使用了Inverted resblock,整个mobilenetv2都由Inverted resblock组成。
Inverted resblock可以分为两个部分:
左边是主干部分,首先利用1x1卷积进行升维,然后利用3x3深度可分离卷积进行特征提取,然后再利用1x1卷积降维。
右边是残差边部分,输入和输出直接相接。
憨批的语义分割重制版|憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台
文章图片

需要注意的是,在DeeplabV3当中,一般不会5次下采样,可选的有3次下采样和4次下采样,本文使用的4次下采样。这里所提到的下采样指的是不会进行五次长和宽的压缩,通常选用三次或者四次长和宽的压缩。
憨批的语义分割重制版|憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台
文章图片

在完成MobilenetV2的特征提取后,我们可以获得两个有效特征层,一个有效特征层是输入图片高和宽压缩两次的结果,一个有效特征层是输入图片高和宽压缩四次的结果。
import math import osimport torch import torch.nn as nn import torch.utils.model_zoo as model_zooBatchNorm2d = nn.BatchNorm2ddef conv_bn(inp, oup, stride): return nn.Sequential( nn.Conv2d(inp, oup, 3, stride, 1, bias=False), BatchNorm2d(oup), nn.ReLU6(inplace=True) )def conv_1x1_bn(inp, oup): return nn.Sequential( nn.Conv2d(inp, oup, 1, 1, 0, bias=False), BatchNorm2d(oup), nn.ReLU6(inplace=True) )class InvertedResidual(nn.Module): def __init__(self, inp, oup, stride, expand_ratio): super(InvertedResidual, self).__init__() self.stride = stride assert stride in [1, 2]hidden_dim = round(inp * expand_ratio) self.use_res_connect = self.stride == 1 and inp == oupif expand_ratio == 1: self.conv = nn.Sequential( # dw nn.Conv2d(hidden_dim, hidden_dim, 3, stride, 1, groups=hidden_dim, bias=False), BatchNorm2d(hidden_dim), nn.ReLU6(inplace=True), # pw-linear nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False), BatchNorm2d(oup), ) else: self.conv = nn.Sequential( # pw nn.Conv2d(inp, hidden_dim, 1, 1, 0, bias=False), BatchNorm2d(hidden_dim), nn.ReLU6(inplace=True), # dw nn.Conv2d(hidden_dim, hidden_dim, 3, stride, 1, groups=hidden_dim, bias=False), BatchNorm2d(hidden_dim), nn.ReLU6(inplace=True), # pw-linear nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False), BatchNorm2d(oup), )def forward(self, x): if self.use_res_connect: return x + self.conv(x) else: return self.conv(x)class MobileNetV2(nn.Module): def __init__(self, n_class=1000, input_size=224, width_mult=1.): super(MobileNetV2, self).__init__() block = InvertedResidual input_channel = 32 last_channel = 1280 interverted_residual_setting = [ # t, c, n, s [1, 16, 1, 1], [6, 24, 2, 2], [6, 32, 3, 2], [6, 64, 4, 2], [6, 96, 3, 1], [6, 160, 3, 2], [6, 320, 1, 1], ]# building first layer assert input_size % 32 == 0 input_channel = int(input_channel * width_mult) self.last_channel = int(last_channel * width_mult) if width_mult > 1.0 else last_channel self.features = [conv_bn(3, input_channel, 2)] # building inverted residual blocks for t, c, n, s in interverted_residual_setting: output_channel = int(c * width_mult) for i in range(n): if i == 0: self.features.append(block(input_channel, output_channel, s, expand_ratio=t)) else: self.features.append(block(input_channel, output_channel, 1, expand_ratio=t)) input_channel = output_channel # building last several layers self.features.append(conv_1x1_bn(input_channel, self.last_channel)) # make it nn.Sequential self.features = nn.Sequential(*self.features)# building classifier self.classifier = nn.Sequential( nn.Dropout(0.2), nn.Linear(self.last_channel, n_class), )self._initialize_weights()def forward(self, x): x = self.features(x) x = x.mean(3).mean(2) x = self.classifier(x) return xdef _initialize_weights(self): for m in self.modules(): if isinstance(m, nn.Conv2d): n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels m.weight.data.normal_(0, math.sqrt(2. / n)) if m.bias is not None: m.bias.data.zero_() elif isinstance(m, BatchNorm2d): m.weight.data.fill_(1) m.bias.data.zero_() elif isinstance(m, nn.Linear): n = m.weight.size(1) m.weight.data.normal_(0, 0.01) m.bias.data.zero_()def load_url(url, model_dir='./model_data', map_location=None): if not os.path.exists(model_dir): os.makedirs(model_dir) filename = url.split('/')[-1] cached_file = os.path.join(model_dir, filename) if os.path.exists(cached_file): return torch.load(cached_file, map_location=map_location) else: return model_zoo.load_url(url,model_dir=model_dir)def mobilenetv2(pretrained=False, **kwargs): model = MobileNetV2(n_class=1000, **kwargs) if pretrained: model.load_state_dict(load_url('http://sceneparsing.csail.mit.edu/model/pretrained_resnet/mobilenet_v2.pth.tar'), strict=False) return model

2、加强特征提取结构
憨批的语义分割重制版|憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台
文章图片

在DeeplabV3+中,加强特征提取网络可以分为两部分:
在Encoder中,我们会对压缩四次的初步有效特征层利用并行的Atrous Convolution,分别用不同rate的Atrous Convolution进行特征提取,再进行合并,再进行1x1卷积压缩特征。憨批的语义分割重制版|憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台
文章图片

在Decoder中,我们会对压缩两次的初步有效特征层利用1x1卷积调整通道数,再和空洞卷积后的有效特征层上采样的结果进行堆叠,在完成堆叠后,进行两次深度可分离卷积块。
憨批的语义分割重制版|憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台
文章图片

这个时候,我们就获得了一个最终的有效特征层,它是整张图片的特征浓缩。
import torch import torch.nn as nn import torch.nn.functional as F from nets.xception import xception from nets.mobilenetv2 import mobilenetv2class MobileNetV2(nn.Module): def __init__(self, downsample_factor=8, pretrained=True): super(MobileNetV2, self).__init__() from functools import partialmodel = mobilenetv2(pretrained) self.features = model.features[:-1]self.total_idx = len(self.features) self.down_idx = [2, 4, 7, 14]if downsample_factor == 8: for i in range(self.down_idx[-2], self.down_idx[-1]): self.features[i].apply( partial(self._nostride_dilate, dilate=2) ) for i in range(self.down_idx[-1], self.total_idx): self.features[i].apply( partial(self._nostride_dilate, dilate=4) ) elif downsample_factor == 16: for i in range(self.down_idx[-1], self.total_idx): self.features[i].apply( partial(self._nostride_dilate, dilate=2) )def _nostride_dilate(self, m, dilate): classname = m.__class__.__name__ if classname.find('Conv') != -1: if m.stride == (2, 2): m.stride = (1, 1) if m.kernel_size == (3, 3): m.dilation = (dilate//2, dilate//2) m.padding = (dilate//2, dilate//2) else: if m.kernel_size == (3, 3): m.dilation = (dilate, dilate) m.padding = (dilate, dilate)def forward(self, x): low_level_features = self.features[:4](x) x = self.features[4:](low_level_features) return low_level_features, x #-----------------------------------------# #ASPP特征提取模块 #利用不同膨胀率的膨胀卷积进行特征提取 #-----------------------------------------# class ASPP(nn.Module): def __init__(self, dim_in, dim_out, rate=1, bn_mom=0.1): super(ASPP, self).__init__() self.branch1 = nn.Sequential( nn.Conv2d(dim_in, dim_out, 1, 1, padding=0, dilation=rate,bias=True), nn.BatchNorm2d(dim_out, momentum=bn_mom), nn.ReLU(inplace=True), ) self.branch2 = nn.Sequential( nn.Conv2d(dim_in, dim_out, 3, 1, padding=6*rate, dilation=6*rate, bias=True), nn.BatchNorm2d(dim_out, momentum=bn_mom), nn.ReLU(inplace=True), ) self.branch3 = nn.Sequential( nn.Conv2d(dim_in, dim_out, 3, 1, padding=12*rate, dilation=12*rate, bias=True), nn.BatchNorm2d(dim_out, momentum=bn_mom), nn.ReLU(inplace=True), ) self.branch4 = nn.Sequential( nn.Conv2d(dim_in, dim_out, 3, 1, padding=18*rate, dilation=18*rate, bias=True), nn.BatchNorm2d(dim_out, momentum=bn_mom), nn.ReLU(inplace=True), ) self.branch5_conv = nn.Conv2d(dim_in, dim_out, 1, 1, 0,bias=True) self.branch5_bn = nn.BatchNorm2d(dim_out, momentum=bn_mom) self.branch5_relu = nn.ReLU(inplace=True)self.conv_cat = nn.Sequential( nn.Conv2d(dim_out*5, dim_out, 1, 1, padding=0,bias=True), nn.BatchNorm2d(dim_out, momentum=bn_mom), nn.ReLU(inplace=True), ) def forward(self, x): [b, c, row, col] = x.size() #-----------------------------------------# #一共五个分支 #-----------------------------------------# conv1x1 = self.branch1(x) conv3x3_1 = self.branch2(x) conv3x3_2 = self.branch3(x) conv3x3_3 = self.branch4(x) #-----------------------------------------# #第五个分支,全局平均池化+卷积 #-----------------------------------------# global_feature = torch.mean(x,2,True) global_feature = torch.mean(global_feature,3,True) global_feature = self.branch5_conv(global_feature) global_feature = self.branch5_bn(global_feature) global_feature = self.branch5_relu(global_feature) global_feature = F.interpolate(global_feature, (row, col), None, 'bilinear', True)#-----------------------------------------# #将五个分支的内容堆叠起来 #然后1x1卷积整合特征。 #-----------------------------------------# feature_cat = torch.cat([conv1x1, conv3x3_1, conv3x3_2, conv3x3_3, global_feature], dim=1) result = self.conv_cat(feature_cat) return resultclass DeepLab(nn.Module): def __init__(self, num_classes, backbone="mobilenet", pretrained=True, downsample_factor=16): super(DeepLab, self).__init__() if backbone=="xception": #----------------------------------# #获得两个特征层 #浅层特征[128,128,256] #主干部分[30,30,2048] #----------------------------------# self.backbone = xception(downsample_factor=downsample_factor, pretrained=pretrained) in_channels = 2048 low_level_channels = 256 elif backbone=="mobilenet": #----------------------------------# #获得两个特征层 #浅层特征[128,128,24] #主干部分[30,30,320] #----------------------------------# self.backbone = MobileNetV2(downsample_factor=downsample_factor, pretrained=pretrained) in_channels = 320 low_level_channels = 24 else: raise ValueError('Unsupported backbone - `{}`, Use mobilenet, xception.'.format(backbone))#-----------------------------------------# #ASPP特征提取模块 #利用不同膨胀率的膨胀卷积进行特征提取 #-----------------------------------------# self.aspp = ASPP(dim_in=in_channels, dim_out=256, rate=16//downsample_factor)#----------------------------------# #浅层特征边 #----------------------------------# self.shortcut_conv = nn.Sequential( nn.Conv2d(low_level_channels, 48, 1), nn.BatchNorm2d(48), nn.ReLU(inplace=True) )self.cat_conv = nn.Sequential( nn.Conv2d(48+256, 256, 3, stride=1, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True), nn.Dropout(0.5),nn.Conv2d(256, 256, 3, stride=1, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True),nn.Dropout(0.1), ) self.cls_conv = nn.Conv2d(256, num_classes, 1, stride=1)def forward(self, x): H, W = x.size(2), x.size(3) #-----------------------------------------# #获得两个特征层 #浅层特征-进行卷积处理 #主干部分-利用ASPP结构进行加强特征提取 #-----------------------------------------# low_level_features, x = self.backbone(x) x = self.aspp(x) low_level_features = self.shortcut_conv(low_level_features)#-----------------------------------------# #将加强特征边上采样 #与浅层特征堆叠后利用卷积进行特征提取 #-----------------------------------------# x = F.interpolate(x, size=(low_level_features.size(2), low_level_features.size(3)), mode='bilinear', align_corners=True) x = self.cat_conv(torch.cat((x, low_level_features), dim=1)) x = self.cls_conv(x) x = F.interpolate(x, size=(H, W), mode='bilinear', align_corners=True) return x

3、利用特征获得预测结果
利用1、2步,我们可以获取输入进来的图片的特征,此时,我们需要利用特征获得预测结果。
利用特征获得预测结果的过程可以分为2步:
1、利用一个1x1卷积进行通道调整,调整成Num_Classes。
2、利用resize进行上采样使得最终输出层,宽高和输入图片一样。
憨批的语义分割重制版|憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台
文章图片

class DeepLab(nn.Module): def __init__(self, num_classes, backbone="mobilenet", pretrained=True, downsample_factor=16): super(DeepLab, self).__init__() if backbone=="xception": #----------------------------------# #获得两个特征层 #浅层特征[128,128,256] #主干部分[30,30,2048] #----------------------------------# self.backbone = xception(downsample_factor=downsample_factor, pretrained=pretrained) in_channels = 2048 low_level_channels = 256 elif backbone=="mobilenet": #----------------------------------# #获得两个特征层 #浅层特征[128,128,24] #主干部分[30,30,320] #----------------------------------# self.backbone = MobileNetV2(downsample_factor=downsample_factor, pretrained=pretrained) in_channels = 320 low_level_channels = 24 else: raise ValueError('Unsupported backbone - `{}`, Use mobilenet, xception.'.format(backbone))#-----------------------------------------# #ASPP特征提取模块 #利用不同膨胀率的膨胀卷积进行特征提取 #-----------------------------------------# self.aspp = ASPP(dim_in=in_channels, dim_out=256, rate=16//downsample_factor)#----------------------------------# #浅层特征边 #----------------------------------# self.shortcut_conv = nn.Sequential( nn.Conv2d(low_level_channels, 48, 1), nn.BatchNorm2d(48), nn.ReLU(inplace=True) )self.cat_conv = nn.Sequential( nn.Conv2d(48+256, 256, 3, stride=1, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True), nn.Dropout(0.5),nn.Conv2d(256, 256, 3, stride=1, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True),nn.Dropout(0.1), ) self.cls_conv = nn.Conv2d(256, num_classes, 1, stride=1)def forward(self, x): H, W = x.size(2), x.size(3) #-----------------------------------------# #获得两个特征层 #浅层特征-进行卷积处理 #主干部分-利用ASPP结构进行加强特征提取 #-----------------------------------------# low_level_features, x = self.backbone(x) x = self.aspp(x) low_level_features = self.shortcut_conv(low_level_features)#-----------------------------------------# #将加强特征边上采样 #与浅层特征堆叠后利用卷积进行特征提取 #-----------------------------------------# x = F.interpolate(x, size=(low_level_features.size(2), low_level_features.size(3)), mode='bilinear', align_corners=True) x = self.cat_conv(torch.cat((x, low_level_features), dim=1)) x = self.cls_conv(x) x = F.interpolate(x, size=(H, W), mode='bilinear', align_corners=True) return x

二、训练部分 1、训练文件详解
我们使用的训练文件采用VOC的格式。
语义分割模型训练的文件分为两部分。
第一部分是原图,像这样:
憨批的语义分割重制版|憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台
文章图片

第二部分标签,像这样:
憨批的语义分割重制版|憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台
文章图片

原图就是普通的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]。
计算公式如下:
憨批的语义分割重制版|憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台
文章图片

就是预测结果和真实结果的交乘上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]) - tpscore = ((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_CEdef CE(): def _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)) # dice_loss = tf.Print(CE_loss, [CE_loss]) return CE_loss return _CE

训练自己的DeeplabV3+模型 首先前往Github下载对应的仓库,下载完后利用解压软件解压,之后用编程软件打开文件夹。
注意打开的根目录必须正确,否则相对目录不正确的情况下,代码将无法运行。
一定要注意打开后的根目录是文件存放的目录。
憨批的语义分割重制版|憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台
文章图片

一、数据集的准备 本文使用VOC格式进行训练,训练前需要自己制作好数据集,如果没有自己的数据集,可以通过Github连接下载VOC12+07的数据集尝试下。
训练前将图片文件放在VOCdevkit文件夹下的VOC2007文件夹下的JPEGImages中。
训练前将标签文件放在VOCdevkit文件夹下的VOC2007文件夹下的SegmentationClass中。
憨批的语义分割重制版|憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台
文章图片

二、数据集的处理 在完成数据集的摆放之后,我们需要对数据集进行下一步的处理,目的是获得训练用的train.txt以及val.txt,需要用到根目录下的voc_annotation.py。
如果下载的是我上传的voc数据集,那么就不需要运行根目录下的voc_annotation.py。
如果是自己制作的数据集,那么需要运行根目录下的voc_annotation.py,从而生成train.txt和val.txt。
憨批的语义分割重制版|憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台
文章图片

三、开始网络训练 通过voc_annotation.py我们已经生成了train.txt以及val.txt,此时我们可以开始训练了。训练的参数较多,大家可以在下载库后仔细看注释,其中最重要的部分依然是train.py里的num_classes。
num_classes用于指向检测类别的个数+1!训练自己的数据集必须要修改!
除此之外在train.py文件夹下面,选择自己要使用的主干模型backbone、预训练权重model_path和下采样因子downsample_factor。预训练模型需要和主干模型相对应。下采样因子可以在8和16中选择。
憨批的语义分割重制版|憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台
文章图片

之后就可以开始训练了。
憨批的语义分割重制版|憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台
文章图片

四、训练结果预测 训练结果预测需要用到两个文件,分别是deeplab.py和predict.py。
我们首先需要去deeplab.py里面修改model_path以及num_classes,这两个参数必须要修改。
【憨批的语义分割重制版|憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台】model_path指向训练好的权值文件,在logs文件夹里。
num_classes指向检测类别的个数+1。
憨批的语义分割重制版|憨批的语义分割重制版9——Pytorch 搭建自己的DeeplabV3+语义分割平台
文章图片

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

    推荐阅读