本文主要介绍如何使用自己的数据集训练DeepLabv3+分割算法,代码使用的是官方源码。
当前使用TensorFlow版本的官方源码,选择它的原因是因为代码中的内容比较全面,除了代码实现以外,还提供了许多文档帮助理解与使用,同时还提供了模型转换的代码实现。
代码地址: 【github】models/research/deeplab at master · tensorflow/models
接下来,先对这个代码仓库进行一下简单的介绍,因为自己在使用该代码仓库的时候只关心训练代码的实现,而忽略的其他的内容,走了不少弯路,到后面才发现我想要的内容,仓库里面早有(==)。
在当前的实现中,我们支持采用以下网络主干:
MobileNetv2
和MobileNetv3
:一个为移动设备设计的快速网络结构Xception
:用于服务器端部署的强大网络结构ResNet-v1-{50, 101}
:我们提供原始的ResNet-v1
及其“ beta”
变体,其中对“ stem”
进行了修改以进行语义分割。PNASNet
: 一个通过神经体系结构搜索发现的强大网络结构。Auto-Deeplab
(代码中叫做HNASNet
):通过神经体系结构搜索找到的特定于细分的网络主干。该目录包含TensorFlow 实现。我们提供的代码使用户可以训练模型,根据mIOU
(平均交叉点求和)评估结果以及可视化细分结果。我们以PASCAL VOC 2012
和Cityscapes
语义分割基准为例。
代码中几个重要文件:
datasets/
:该文件夹下包含对于训练数据集的处理代码,主要针对 PASCAL VOC 2012
和Cityscapes
数据集的处理。g3doc/
:该文件夹下包含多个Markdown
文件,非常有用,如何安装,常见问题FAQ等。deeplab_demo.ipynb
:该文件中给出了如果对一张图像进行语义分割并显示结果的Demo。export_model.py
:该文件提供了将训练的checkpoint
模型转为.pb
文件的代码实现。train.py
:训练代码文件,训练时,需要指定提供的训练参数。eval.py
:验证代码,输出mIOU,用来评估模型的好坏。vis.py
:可视化代码。Deeplab
依赖的库有:
PYTHONPATH
本地运行的时候,tensorflow/models/research/
目录应该追加到PYTHONPATH
中,如下:
# From tensorflow/models/research/ export PYTHONPATH=$PYTHONPATH:`pwd`:`pwd`/slim # [Optional] for panoptic evaluation, you might need panopticapi: # https://github.com/cocodataset/panopticapi # Please clone it to a local directory ${PANOPTICAPI_DIR} touch ${PANOPTICAPI_DIR}/panopticapi/__init__.py export PYTHONPATH=$PYTHONPATH:${PANOPTICAPI_DIR}/panopticapi 复制代码
注意:此命令需要在您启动的每个新终端上运行。如果希望避免手动运行此命令,可以将它作为新行添加到
〜/ .bashrc
文件的末尾。
通过运行model_test.py
快速测试:
# From tensorflow/models/research/ python deeplab/model_test.py 复制代码
在PASCAL VOC 2012
数据集上快速运行所有代码:
# From tensorflow/models/research/deeplab sh local_test.sh 复制代码
最终目标: 生成TFRecord
格式的数据
数据集目录结构如下:
+dataset #数据集名称 +image +mask +index - train.txt - trainval.txt - val.txt +tfrecord 复制代码
.jpg
和.png
都可以,只要在代码中读取一致即可。VOC数据集默认原图是.jpg
,mask图像为.png
。txt
文件(不加后缀)tfrecord
格式的图像数据数据集制作流程:
mask
图像TFRecord
格式的数据集训练集数据包含两部分,一是原图,二是对应分类的标注值(本文中称为mask
图像)。
mask图像的值是如何设置的?
根据图像分割的分类个数来制作原图对应的mask图像。假如一共有N个类别(背景作为一类),则mask图像的值的范围是[0~N)
。0
值作为背景值,其他分割类别的值依次设置为1, 2, ..., N-1
。
注意:
ignore_label
:从字面意思来讲是忽略的标签,即ignore_label
是指没有做标注的像素,即不需进行预测的像素值,因此,它不参与loss
值的计算,在mask
图像中将其值记为255
。mask
图像是单通道的灰度图像。mask
图像的格式没有限定,但所有的mask图像采用同一种图像格式,方便数据读取。小总结
mask
图像的值分为三类:
0
表示1, 2, ....., N-1
表示ignore_label
值:用255
表示如果分割的类别较少,则生成的
mask
图像看上去是一片黑,因为分类的值都较小,在0~255
的范围内不容易显示出来。
这部分就是将准备的数据集进行分割,分为训练集、验证集、测试集。 无需将具体的图像文件分到三个文件夹中,只需要建立图像的索引文件即可,通过添加相应的路径+文件名即可获取到具体的图像。
假设原图像和mask图像的存放路径如下:
./dataset/images
mask
图像:./dataset/mask
:此处存放的是2.1小节要求格式原图与
mask
图像是一一对应的,包括图像尺寸,图像名(后缀可以不同)
索引文件存放路径:./dataset/index
,该路径下生成:
train.txt
trainval.txt
val.txt
索引文件中,只需记录文件名(不加后缀),这取决于代码中数据集加载的方式。
TFRecord
格式
TFRecord
是谷歌推荐的一种二进制文件格式,理论上它可以保存任何格式的信息。TFRecord
内部使用了“Protocol Buffer”
二进制数据编码方案,它只占用一个内存块,只需要一次性加载一个二进制文件的方式即可,简单,快速,尤其对大型训练数据很友好。而且当我们的训练数据量比较大的时候,可以将数据分成多个TFRecord文件,来提高处理效率。
那么,如何将数据生成TFRecord
格式呢?
在此,我们可以借助 项目代码中./datasets/build_voc2012_data.py
文件来实现。给文件是VOC2012数据集处理的代码,我们只需修改一下输入参数即可。
参数:
image_folder
:原图文件夹名称,./dataset/image
semantic_segmentation_folder
:分割文件夹名称, ./dataset/mask
list_folder
:索引文件夹名称,./dataset/index
output_dir
:输出路径,即生成的tfrecord
文件所在位置,./dataset/tfrecord
运行命令:
python ./datasets/build_voc2012_data.py --image_folder=./dataset/image --semantic_segmentation_folder=./dataset/mask --list_folder=./dataset/index --output_dir=./dataset/tfrecord 复制代码
生成的文件如下:
注意: 可在代码中调节参数_NUM_SHARDS
(默认为4
),改变数据分块的数目。(一些文件系统有最大单个文件大小的限制,如果数据集非常大,增加_NUM_SHARDS
可减小单个文件的大小)
该文件的核心代码如下:
# dataset_split指的是train.txt, val.txt等 dataset = os.path.basename(dataset_split)[:-4] filenames = [x.strip('\n') for x in open(dataset_split, 'r')] # 文件名列表 # 输出tfrecord文件名 output_filename = os.path.join( FLAGS.output_dir, '%s-%05d-of-%05d.tfrecord' % (dataset, shard_id, _NUM_SHARDS)) with tf.python_io.TFRecordWriter(output_filename) as tfrecord_writer: for i in range(start_idx, end_idx): image_filename = os.path.join(iamge_folder, filenames[i]+'.'+image_format)# 原图路径 image_data = tf.gfile.GFile(image_filename, 'rb').read() #读取原图文件 height, width = image_reader.read_image_dims(image_data) seg_filename = os.path.join(semantic_segmentation_folder, filenames[i] + '.' + label_format) # mask图像路径 seg_data = tf.gfile.GFile(seg_filename, 'rb').read() # 读取分割图像 seg_height, seg_width = label_reader.read_image_dims(seg_data) # 判断原图与mask图像尺寸是否匹配 if height != seg_height or width != seg_width: raise RuntimeError('Shape mismatched between image and label.') # Convert to tf example. example = build_data.image_seg_to_tfexample( image_data, filenames[i], height, width, seg_data) tfrecord_writer.write(example.SerializeToString()) 复制代码
至此,数据集的制作部分已经完成!!!
为了训练自己的数据集,需要修改以下几处文件:
datasets/data_generator.py
:增加数据集的注册该文件提供语义分割数据的包装器
在该文件中,可以看到PASCAL_VOC
, CITYSCAPES
以及ADE20K
数据集的数据描述,如下:
_PASCAL_VOC_SEG_INFORMATION = DatasetDescriptor( splits_to_sizes={ 'train': 1464, 'train_aug': 10582, 'trainval': 2913, 'val': 1449, }, num_classes=21, ignore_label=255, ) 复制代码
en,比着葫芦画瓢,增加我们自己数据集的描述信息,如下:
_PORTRAIT_INFORMATION = DatasetDescriptor( splits_to_sizes={ 'train': 17116, 'trainval': 21395, 'val': 4279, }, num_classes=2, # 类别数目,包括背景 ignore_label=255, # 忽略像素值 ) 复制代码
以人像分割任务为例,只有两类,即前景(人像)和背景(非人像)。
添加完描述信息后,需要将该数据集信息进行注册,如下:
_DATASETS_INFORMATION = { 'cityscapes': _CITYSCAPES_INFORMATION, 'pascal_voc_seg': _PASCAL_VOC_SEG_INFORMATION, 'ade20k': _ADE20K_INFORMATION, 'portrait_seg': _PORTRAIT_INFORMATION, #增加此句 } 复制代码
注意:此处的数据集名称要与前面对应!
./utils/train_utils.py
修改在函数get_model_init_fn
中,修改为如下代码,增加logits
层不加载预训练模型权重:
# Variables that will not be restored. exclude_list = ['global_step', 'logits'] if not initialize_last_layer: exclude_list.extend(last_layers) 复制代码
训练文件train.py
和common.py
文件中包含了训练分割网络所需要的所有参数。
model_variant
:Deeplab
模型变量,可选值可见core/feature_extractor.py
。
mobilenet_v2
时,设置变量strous_rates=decoder_output_stride=None
;xception_65
或resnet_v1
时,设置strous_rates=[6,12,18](output stride 16), decoder_output_stride=4
。label_weights
:此变量可以设置标签的权重值,当数据集中出现类别不均衡时,可通过此变量来指定每个类别标签的权重值,如label_weights=[0.1, 0.5]
意味着标签0的权重是0.1, 标签1的权重是0.5。如果该值为None
,则所有的标签具有相同的权重1.0
。train_logdir
:存放checkpoint
和logs
的路径。log_steps
:该值表示每隔多少步输出日志信息。save_interval_secs
:该值表示以秒为单位,每隔多长时间保存一次模型文件到硬盘。optimizer
:优化器,可选值['momentum', 'adam']
。learning_policy
:学习率策略,可选值['poly', 'step']
。base_learning_rate
:基础学习率,默认值0.0001
。training_number_of_steps
:模型训练的迭代次数。train_batch_size
:模型训练的批处理图像数量。train_crop_size
:模型训练时所使用的图像尺寸,默认'513, 513'
。tf_initial_checkpoint
:预训练模型。initialize_last_layer
:是否初始化最后一层。last_layers_contain_logits_only
:是否只考虑逻辑层作为最后一层。fine_tune_batch_norm
:是否微调batch norm
参数。atrous_rates
:默认值[6, 12, 18]
。output_stride
:默认值16
,输入和输出空间分辨率的比值
xception_65
, 如果output_stride=8
,则使用atrous_rates=[12, 24, 36]
output_stride=16
,则atrous_rates=[6, 12, 18]
mobilenet_v2
,使用None
strous_rates
和output_stride
。dataset
:所使用的分割数据集,此处与数据集注册时的名称一致。train_split
:使用哪个数据集来训练,可选值即数据集注册时的值,如train
, trainval
。dataset_dir
:数据集存放的路径。针对训练参数,下面几点需要重点注意:
关于是否加载预训练网络的权重问题 如果要在其他数据集上微调该网络,需要关注以下几个参数:
initialize_last_layer=True
backbone
,设置initialize_last_layer=False
和last_layers_contain_logits_only=False
logits
,设置initialize_last_layer=False
和last_layers_contain_logits_only=True
由于我的数据集分类与默认类别数不同,因此采取的参数值是:
--initialize_last_layer=false --last_layers_contain_logits_only=true 复制代码
如果资源有限,想要训练自己数据集的几条建议:
output_stride=16
或者甚至32
(同时需要修改atrous_rates
变量,例如,对于output_stride=32
,atrous_rates=[3, 6, 9]
)GPU
,更改num_clone
标志,并将train_batch_size
设置的尽可能大train_crop_size
,可以将它设置的更小一些,例如513x513
(甚至321x321
),这样就可以使用更大的batch_size
mobilenet_v2
关于是否微调batch_norm
当训练使用的批处理大小train_batch_size
大于12(最好大于16)时,设置fine_tune_batch_norm=True
。否则,设置fine_tune_batch_norm=False
。
模型链接具体可见:models/model_zoo.md at master · tensorflow/models
提供了在几个数据集上的预训练模型,包括(1) PASCAL VOC 2012
, (2) Cityscapes
, (3) ADE20K
未解压的目下包括:
frozen inference graph
(forzen_inference_graph.pb
)。默认情况下,所有冻结推理图的输出步长为8,单个eval scale为1.0,没有左右翻转,除非另外指定。基于MobileNet-v2
的模型不包括解码器模块。checkpoint
(model.ckpt.data-00000-of-00001
, model.ckpt.index
)还提供了在ImageNet
预训练的checkpoints
未解压文件包括:
一个model checkpoint (model.ckpt.data-00000-of-00001, model.ckpt.index)
根据自己的情况进行下载!
python train.py \ --logtostderr \ --training_number_of_steps=20000 \ --train_split="train" \ --model_variant="xception_65" \ --train_crop_size="513,513" \ --atrous_rates=6 \ --atrous_rates=12 \ --atrous_rates=18 \ --output_stride=16 \ --decoder_output_stride=4 \ --train_batch_size=2 \ --save_interval_secs=240 \ --optimizer="momentum" \ --leraning_policy="poly" \ --fine_tune_batch_norm=false \ --initialize_last_layer=false \ --last_layers_contain_logits_only=true \ --dataset="portrait_seg" \ --tf_initial_checkpoint="./checkpoint/deeplabv3_pascal_trainval/model.ckpt" \ --train_logdir="./train_logs" \ --dataset_dir="./dataset/tfrecord" 复制代码
验证代码: ./eval.py
# From tensorflow/models/research/ python deeplab/eval.py \ --logtostderr \ --eval_split="val" \ --model_variant="xception_65" \ --atrous_rates=6 \ --atrous_rates=12 \ --atrous_rates=18 \ --output_stride=16 \ --decoder_output_stride=4 \ --eval_crop_size="513,513" \ --dataset="portrait_seg" \ # 数据集名称 --checkpoint_dir=${PATH_TO_CHECKPOINT} \ # 预训练模型 --eval_logdir=${PATH_TO_EVAL_DIR} \ --dataset_dir="./dataset/tfrecord" # 数据集路径 复制代码
得到的结果如下:
可以使用Tensorboard
检查培训和评估工作的进展。如果使用推荐的目录结构,Tensorboard可以使用以下命令运行:
tensorboard --logdir=${PATH_TO_LOG_DIRECTORY} # 文中log地址 tensorboard --logdir="./train_logs" 复制代码
在训练过程中,会保存模型文件到硬盘,如下:
其形式是TensorFlow
的checkpoint
格式,代码中提供了一个脚本(export_model.py
)可以将checkpoint
转换为.pb
格式。
export_model.py
主要参数:
checkpoint_path
:训练保存的检查点文件export_path
:模型导出路径num_classes
:分类类别crop_size
:图像尺寸,[513, 513]
atrous_rates
:12, 24, 36
output_stride
:8
生成的.pb
文件如下:
class DeepLabModel(object): """class to load deeplab model and run inference""" INPUT_TENSOR_NAME = 'ImageTensor:0' OUTPUT_TENSOR_NAME='SemanticPredictions:0' INPUT_SIZE = 513 FROZEN_GRAPH_NAME= 'frozen_inference_graph' def __init__(self, pretrained_weights): """Creates and loads pretrained deeplab model.""" self.graph = tf.Graph() graph_def = None # Extract frozen graph from tar archive if pretrained_weights.endswith('.tar.gz'): tar_file = tarfile.open(pretrained_weights) for tar_info in tar_file.getmembers(): if self.FROZEN_GRAPH_NAME in os.path.basename(tar_info.name): file_handle = tar_file.extractfile(tar_info) graph_def = tf.GraphDef.FromString(file_handle.read()) break tar_file.close() else: with open(pretrained_weights, 'rb') as fd: graph_def = tf.GraphDef.FromString(fd.read()) if graph_def is None: raise RuntimeError('Cannot find inference graph in tar archive.') with self.graph.as_default(): tf.import_graph_def(graph_def, name='') gpu_options = tf.GPUOptions(allow_growth=True) config = tf.ConfigProto(gpu_options=gpu_options, log_device_placement=False) self.sess = tf.Session(graph=self.graph, config=config) def run(self, image): """Runs inference on a single image. Args: image: A PIL.Image object, raw input image. Returns: resized_image:RGB image resized from original input image. seg_map:Segmentation map of 'resized_iamge'. """ width, height = image.size resize_ratio = 1.0 * self.INPUT_SIZE/max(width, height) target_size = (int(resize_ratio*width), int(resize_ratio * height)) resized_image = image.convert('RGB').resize(target_size, Image.ANTIALIAS) batch_seg_map = self.sess.run( self.OUTPUT_TENSOR_NAME, feed_dict={self.INPUT_TENSOR_NAME:[np.asarray(resized_image)]} ) seg_map = batch_seg_map[0] return resized_image, seg_map if __name__ == '__main__': pretrained_weights = './train_logs/frozen_inference_graph_20000.pb' MODEL = DeepLabModel(pretrained_weights) # 加载模型 img_name = 'test.jpg' img = Image.open(img_name) resized_im, seg_map = MODEL.run(original_im) #获取结果 seg_map[seg_map==1]=255 #将人像的像素值置为255 seg_map.save('output.jpg') # 保存mask结果图像 复制代码
至此,整个训练过程就结束了!!!