在搭建好Tensorflow以及TensorFlow的Object Detection API安装后,我们可以运行一些预训练好的模型进行推理运算,并看到一些效果。但有时候我们需要定制自己的分类。例如我只想识别人(person)和人脸(face),或者一些较多或较少的类别,那么这就需要训练自己的分类模型。
在开始之前,可能需要了解深度学习训练的数据。我们从以下内容进行粗略的理解:
数据应该具有以下几个特性:
数据拆分,一般只有训练集和测试集两个子集:
大致采用如下的工作流程:
上图中,“调整模型”指的是调整您可以想到的关于模型的任何方面,从更改学习速率、添加或移除特征,到从头开始设计全新模型。该工作流程结束时,您可以选择在测试集上获得最佳效果的模型。
更加优化的拆分:
通过将数据集划分为三个子集(如下图所示),可以大幅降低过拟合的发生几率:
使用验证集评估训练集的效果。然后,在模型“通过”验证集之后,使用测试集再次检查评估结果。下图展示了这一新工作流程:
需要训练自己的数据集,首先需要先准备好数据。目前,深度学习方面有较多的公开的数据集。常用的有PASCAL VOC、COCO、MNIST、ImageNet等等。这些都是预训练好的数据集,但我们也可以自己训练,只是使用这些数据集中的图片数据,而且我们假定这些图片都是符合数据应该有的特性(来源都是同一个分布,分布随机独立,且稳定)。
参考来源:25个深度学习的开放数据集
首先,我们采用数据量较少的VOC2005的数据集。在准备好数据后,创建如下的目录结构
以下的操作,都会逐个对应到某一个文件夹(请按照序号依次操作)。
存放图片
将VOC2005的图片集放入到JPEGImage文件夹中。存放所有图片,不区分测试、验证和训练的分类。
图片标注
将JPEGImage中的图片,使用labelImg进行逐一标注类似如下界面:
框选需要训练识别的分类,并选择label然后保存(Ctrl+s),labelImg会在图片目录下生产对应文件名的xml文件。本次训练我只识别person
和face
两个类,所以标注的label中也只有这两种分类(后续对应xxx_label_map.pbtxt中需要匹配修改
)。
在将所有图片标注完成后,将对应的xml文件移动到Annotations
文件夹下。
创建图片文件分类脚本
在VOC2005目录下创建 createclassificationfile.py
(文件名不要求)文件,输入以下code:
import os
import random
trainval_percent = 0.66
train_percent = 0.5
xmlfilepath = 'Annotations'
txtsavepath = 'ImageSets\Main'
total_xml = os.listdir(xmlfilepath)
print(total_xml)
num=len(total_xml)
list=range(num)
tv=int(num*trainval_percent)
tr=int(tv*train_percent)
trainval= random.sample(list,tv)
train=random.sample(trainval,tr)
ftrainval = open('ImageSets/Main/trainval.txt', 'w')
ftest = open('ImageSets/Main/test.txt', 'w')
ftrain = open('ImageSets/Main/train.txt', 'w')
fval = open('ImageSets/Main/val.txt', 'w')
for i in list:
name=total_xml[i][:-4]+'\n'
if i in trainval:
ftrainval.write(name)
if i in train:
ftrain.write(name)
else:
fval.write(name)
else:
ftest.write(name)
ftrainval.close()
ftrain.close()
fval.close()
ftest .close()
执行:
$ python createclassificationfile.py
运行完成后,查看ImageSets/Main/
目录下是否有生成如下文件:
如果生成成功,则表示已经将图片集以及对应标注好的xml文件进行了分类。每个txt中都包含了图片集的文件名,不同的txt是不同类别,文件名也很直观。
虽然我们可以把这些训练集和标注好的xml文件放入到Tensorflow object detection API的环境目录下进行训练。但为了保证环境目录的简洁,还是选择将object detection API中的关键文件复制到我们的train_project
下对应的目录中,这样就类似把object detection API的整体环境筛选性的复制出来了。
$cp */tensorflow/models/research/object_detection/dataset_tools/create_pascal_tf_record.py train_project/dataset/
$cp */tensorflow/models/research/object_detection/data/pascal_label_map.pbtxt train_project/dataset/
ps:因为使用的pascal voc数据,所以选择create_pascal_tf_record.py 和pascal_label_map.pbtxt。如果使用MSCOCO数据集,可选择对应的create_coco_tf_record.py和mscoco_label_map.pbtxt文件。
为了保证在train_project下拥有完整的object detection API环境,我们需要把object_dection
中的ssd_inception_v2_coco.config
、export_inference_graph.py
、train.py
、eval.py
和目录utils
拷贝到train_project目录层中,其中:
ssd_inception_v2_coco.config
: 模型的配置文件,可设置训练模型的参数,例如LearnRate、batch_size等。
train.py
:object detection启动模型训练的脚本
eval.py
:验证的脚本
export_inference_graph.py
:导出训练图模型成pb文件使用
utils
:相关工具脚本
PS:以上文件或目录都在*/tensorflow/models/research/object_detection/中,可直接搜索到。
我们选择使用ssd_mobilenet_v1_coco_2018_01_28的模型进行训练。
下载模型,并将模型加压到train_project/models
中,在train_project/models
中则会有文件夹ssd_mobilenet_v1_coco_2018_01_28
,打开可以看到如下文件:
这是一个完整的预训练好的模型,可直接使用。但我们需要训练自己的模型,所以只会用到其中的model.ckpt.index
和model.ckpt.meta
。其余的文件都和使用的数据和需要识别的分类有关,这些文件我们需要自己生成。
我们准备好了自己的训练数据和标注好的xml文件,同时也复制了object detection API的环境。这时,我们需要去修改这些复制过来的环境文件,适配我们现在准备好的新环境,为训练做最后一步准备。
我在修改的地方都注释了原来的代码,从而替换为新的代码,并且添加了中文说明,文件修改如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2017 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
r"""Convert raw PASCAL dataset to TFRecord for object_detection.
Example usage:
python object_detection/dataset_tools/create_pascal_tf_record.py \
--data_dir=/home/user/VOCdevkit \
--year=VOC2012 \
--output_path=/home/user/pascal.record
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import hashlib
import io
import logging
import os
from lxml import etree
import PIL.Image
import tensorflow as tf
from object_detection.utils import dataset_util
from object_detection.utils import label_map_util
#执行参数,我们可以在这里修改,也可以在执行时候带上参数修改,建议在执行时带上参数,这部分没有代码修改
flags = tf.app.flags
flags.DEFINE_string('data_dir', '', 'Root directory to raw PASCAL VOC dataset.')
flags.DEFINE_string('set', 'train', 'Convert training set, validation set or '
'merged set.')
flags.DEFINE_string('annotations_dir', 'Annotations',
'(Relative) path to annotations directory.')
flags.DEFINE_string('year', 'VOC2007', 'Desired challenge year.')
flags.DEFINE_string('output_path', '', 'Path to output TFRecord')
flags.DEFINE_string('label_map_path', 'data/pascal_label_map.pbtxt',
'Path to label map proto')
flags.DEFINE_boolean('ignore_difficult_instances', False, 'Whether to ignore '
'difficult instances')
FLAGS = flags.FLAGS
SETS = ['train', 'val', 'trainval', 'test']
#增加我们自己的数据集VOC2005
#YEARS = ['VOC2007', 'VOC2012', 'merged']
YEARS = ['VOC2007', 'VOC2012', 'merged', 'VOC2005']
#注意最后一个参数image_subdirectory的缺省值,可以在此修改
def dict_to_tf_example(data,
dataset_directory,
label_map_dict,
ignore_difficult_instances=False,
image_subdirectory='PNGImages'):
"""Convert XML derived dict to tf.Example proto.
Notice that this function normalizes the bounding box coordinates provided
by the raw data.
Args:
data: dict holding PASCAL XML fields for a single image (obtained by
running dataset_util.recursive_parse_xml_to_dict)
dataset_directory: Path to root directory holding PASCAL dataset
label_map_dict: A map from string label names to integers ids.
ignore_difficult_instances: Whether to skip difficult instances in the
dataset (default: False).
image_subdirectory: String specifying subdirectory within the
PASCAL dataset directory holding the actual image data.
Returns:
example: The converted tf.Example.
Raises:
ValueError: if the image pointed to by data['filename'] is not a valid JPEG
"""
#确定照片的路径,建议将路径输出,看看是否正确。
#这里有个大坑,官方的XML标注里面,filename字段是后面有文件类型的,但是用labelImg标注是没有的
#我们在img_path里手动拼接 +'.png'
#img_path = os.path.join(data['folder'],image_subdirectory, data['filename'])
img_path = os.path.join('VOC2005', image_subdirectory, data['filename'])
full_path = os.path.join(dataset_directory, img_path)
#手动输入查看路径是否正确,这是为了在执行可能报错的情况下检查路径是否出错
print('dataset_directory',dataset_directory)
print('full_path',full_path)
with tf.gfile.GFile(full_path, 'rb') as fid:
encoded_jpg = fid.read()
encoded_jpg_io = io.BytesIO(encoded_jpg)
image = PIL.Image.open(encoded_jpg_io)
#if image.format != 'JPEG':
# raise ValueError('Image format not JPEG')
key = hashlib.sha256(encoded_jpg).hexdigest()
width = int(data['size']['width'])
height = int(data['size']['height'])
xmin = []
ymin = []
xmax = []
ymax = []
classes = []
classes_text = []
truncated = []
poses = []
difficult_obj = []
if 'object' in data:
for obj in data['object']:
difficult = bool(int(obj['difficult']))
if ignore_difficult_instances and difficult:
continue
difficult_obj.append(int(difficult))
xmin.append(float(obj['bndbox']['xmin']) / width)
ymin.append(float(obj['bndbox']['ymin']) / height)
xmax.append(float(obj['bndbox']['xmax']) / width)
ymax.append(float(obj['bndbox']['ymax']) / height)
classes_text.append(obj['name'].encode('utf8'))
classes.append(label_map_dict[obj['name']])
truncated.append(int(obj['truncated']))
poses.append(obj['pose'].encode('utf8'))
example = tf.train.Example(features=tf.train.Features(feature={
'image/height': dataset_util.int64_feature(height),
'image/width': dataset_util.int64_feature(width),
'image/filename': dataset_util.bytes_feature(
data['filename'].encode('utf8')),
'image/source_id': dataset_util.bytes_feature(
data['filename'].encode('utf8')),
'image/key/sha256': dataset_util.bytes_feature(key.encode('utf8')),
'image/encoded': dataset_util.bytes_feature(encoded_jpg),
'image/format': dataset_util.bytes_feature('jpeg'.encode('utf8')),
'image/object/bbox/xmin': dataset_util.float_list_feature(xmin),
'image/object/bbox/xmax': dataset_util.float_list_feature(xmax),
'image/object/bbox/ymin': dataset_util.float_list_feature(ymin),
'image/object/bbox/ymax': dataset_util.float_list_feature(ymax),
'image/object/class/text': dataset_util.bytes_list_feature(classes_text),
'image/object/class/label': dataset_util.int64_list_feature(classes),
'image/object/difficult': dataset_util.int64_list_feature(difficult_obj),
'image/object/truncated': dataset_util.int64_list_feature(truncated),
'image/object/view': dataset_util.bytes_list_feature(poses),
}))
return example
def main(_):
if FLAGS.set not in SETS:
raise ValueError('set must be in : {}'.format(SETS))
if FLAGS.year not in YEARS:
raise ValueError('year must be in : {}'.format(YEARS))
data_dir = FLAGS.data_dir
#新增我们的数据集到years中
#years = ['VOC2007', 'VOC2012']
years = ['VOC2007', 'VOC2012', 'VOC2005']
if FLAGS.year != 'merged':
years = [FLAGS.year]
writer = tf.python_io.TFRecordWriter(FLAGS.output_path)
label_map_dict = label_map_util.get_label_map_dict(FLAGS.label_map_path)
for year in years:
logging.info('Reading from PASCAL %s dataset.', year)
#修改成如下代码,这里只需要用到Main下面的train.txt,val.txt等4个文件
#原来的代码是用了官方下面的XX_train.txt等文件
#examples_path = os.path.join(data_dir, year, 'ImageSets', 'Main',
# 'aeroplane_' + FLAGS.set + '.txt')
examples_path = os.path.join(data_dir, year, 'ImageSets',
'Main', FLAGS.set + '.txt')
annotations_dir = os.path.join(data_dir, year, FLAGS.annotations_dir)
examples_list = dataset_util.read_examples_list(examples_path)
for idx, example in enumerate(examples_list):
if idx % 100 == 0:
logging.info('On image %d of %d', idx, len(examples_list))
path = os.path.join(annotations_dir, example + '.xml')
with tf.gfile.GFile(path, 'r') as fid:
xml_str = fid.read()
xml = etree.fromstring(xml_str)
data = dataset_util.recursive_parse_xml_to_dict(xml)['annotation']
tf_example = dict_to_tf_example(data, FLAGS.data_dir, label_map_dict,
FLAGS.ignore_difficult_instances)
writer.write(tf_example.SerializeToString())
writer.close()
if __name__ == '__main__':
tf.app.run()
清空文件原来的内容,添加如下代码:
item {
id: 1
name: 'face'
}
item {
id: 2
name: 'person'
}
这里只是针对本次训练的修改,如果你的分类和原本的分类无差别,则不需要清空,只需要将自己添加的类放到文件中即可。
在train_project文件夹中执行以下命令,生成训练集
的record文件pascal_train.record和验证集
的record文件pascal_val.record。
生成pascal_train.record:
$ python dataset/create_pascal_tf_record.py --data_dir=/home/alpha_gpu/train_project/dataset --year=VOC2005 --set=train --output_path=/home/alpha_gpu/train_project/record/pascal_train.record --label_map_path=/home/alpha_gpu/train_project/dataset/pascal_label_map.pbtxt
生成pascal_val.record:
$ python dataset/create_pascal_tf_record.py --data_dir=/alpha_gpu/train_project/dataset --year=VOC2005 --set=val --output_path=/home/alpha_gpu/train_project/record/pascal_val.record --label_map_path=/home/alpha_gpu/train_project/dataset/pascal_label_map.pbtxt
ps:运行过程中可能有报错,大多是路径不匹配造成的。
执行完成后,可以在*/train_project/record/
下看到pascal_train.record
和pascal_val.record
两个文件,如下。
同上,我在修改的地方都注释了原来的代码,从而替换为新的代码,并且添加了中文说明,文件修改如下:
model {
ssd {
#修改分类的个数
#num_classes: 90
num_classes: 2
image_resizer {
fixed_shape_resizer {
height: 300
width: 300
}
}
feature_extractor {
type: "ssd_mobilenet_v1"
depth_multiplier: 1.0
min_depth: 16
conv_hyperparams {
regularizer {
l2_regularizer {
weight: 3.99999989895e-05
}
}
initializer {
truncated_normal_initializer {
mean: 0.0
stddev: 0.0299999993294
}
}
activation: RELU_6
batch_norm {
decay: 0.999700009823
center: true
scale: true
epsilon: 0.0010000000475
train: true
}
}
}
···
···
train_config {
batch_size: 64
data_augmentation_options {
random_horizontal_flip {
}
}
data_augmentation_options {
ssd_random_crop {
}
}
optimizer {
rms_prop_optimizer {
learning_rate {
exponential_decay_learning_rate {
initial_learning_rate: 0.0005
decay_steps: 800720
decay_factor: 0.949999988079
}
}
momentum_optimizer_value: 0.899999976158
decay: 0.899999976158
epsilon: 1.0
}
}
#修改模型文件路径
#fine_tune_checkpoint: "PATH_TO_BE_CONFIGURED/model.ckpt"
fine_tune_checkpoint: "models/ssd_mobilenet_v1_coco_2018_01_28/model.ckpt"
from_detection_checkpoint: true
num_steps: 200000
}
train_input_reader {
#修改label_map的文件路径
#label_map_path: "PATH_TO_BE_CONFIGURED/mscoco_label_map.pbtxt"
label_map_path: "dataset/pascal_label_map.pbtxt"
tf_record_input_reader {
#input_path: "PATH_TO_BE_CONFIGURED/mscoco_train.record"
input_path: "record/pascal_train.record"
}
}
eval_config {
#用例个数,在训练完成后用来验证的参数,num_examples的值请打开train_project/dataset/VOC2005/ImageSets/Main/val.txt,查看验证集的数量。
#num_examples: 8000
num_examples: 68
max_evals: 10
use_moving_averages: false
}
eval_input_reader {
#修改label_map的文件路径
#label_map_path: "PATH_TO_BE_CONFIGURED/mscoco_label_map.pbtxt"
label_map_path: "dataset/pascal_label_map.pbtxt"
shuffle: ture
num_readers: 1
tf_record_input_reader {
#input_path: "PATH_TO_BE_CONFIGURED/mscoco_val.record"
input_path: "record/pascal_val.record"
}
}
在train_project目录下执行:
$ nohup python train.py --train_dir=train \
--model_config_path=/home/alpha_gpu/train_project/ssd_mobilenet_v1_coco.config \
--logtostderr &
这样可以将任务挂在后台运行,且将训练的模型文件保存在/train_project/train/
下。
执行上述命令后,任务会将日志输出到train_project目录下的nohup.out文件。
打开终端,执行tail -f nohup.out
即可查看实时日志。
在成功安装了Tensorflow object detection API后,会有一个tensorboard工具。
在train_project目录下执行:
$ tensorboard --logdir=train
则会出现以下显示:
TensorBoard 1.10.0 at http://alpha-gpu-server:6060 (Press CTAL+C to quit)
打开游览器,输入http://alpha-gpu-server:6060
,即可查看到训练曲线。
在train_project运行
$ python eval.py --checkpoint_dir=train --eval_dir=eval --pipeline_config_path=train/pipeline.config
运行结束后会看到类似如下的验证结果:
Running eval batches done
# success:68
# skipped:0
在train_project运行
$ python export_inference_graph.py --input_type=image_tensor \
--pipeline_config_path=/home/alpha_gpu/train_project/train/pipeline.config \
--trained_checkpoint_prefix=train/model.ckpt-10250 \
--output_directory=resultmodel
则会在train_project/resultmodel下保存如下文件则为保存成功:
下面列出了几条可为您提供指导的经验法则: