tensorflow object detection的简单使用(一)

在上一篇博文中,简单介绍了如何在ubuntu16.04上安装google提供的基于tensorflow的object detection模块。本篇博文简单介绍如何去使用这个模块,主要以训练PASCAL VOC2012数据集为例进行讲解。什么是PASCAL VOC,如果有不了解的同学可以抽点时间看下这篇博文这个博主讲的很详细,简单来说就是拥有20个类别的目标检测数据集(当然除了目标检测还有目标分割等数据),包括人、车、船、飞机等常见的20类物体。

首先简单介绍下本次训练的流程(默认已安装好object detection模块):

1.下载PASCAL VOC2012数据集,并将数据集转为tfrecord格式

2.选择并下载预训练模型

3.配置训练文件configuration

4.训练模型

5.利用tensorboard查看训练过程中loss,accuracy等变化曲线

6.冻结模型参数

7.调用冻结pb文件进行预测

 

1.下载数据集与转换格式

在该项目开始之前,我们先将文件结构建立起来(培养文件归类的好习惯),根据官方的建议,文件结构如下,建立好文件结构后我们将pascal_lable_map.pbtxt标签文件(里面存放着id以及对应的目标名称)放到data目录下,该文件在/xxx/models-master/research/object_detection/data目录下:

+data
  -label_map file
  -train TFRecord file
  -eval TFRecord file
+models
  + model
    -pipeline config file
    +train
    +eval

在终端通过以下指令复制(有图形界面的同学直接复制粘贴就行):

cp /xxx/models-master/research/object_detection/data/pascal_label_map.pbtxt /xxx/xxx/data/

接下来直接用以下指令在ubuntu终端上下载,或者复制下面的链接到浏览器上也能下,大概有1.2G的样子,下载完后放到data文件夹下。

wget http://host.robots.ox.ac.uk/pascal/VOC/voc2012/VOCtrainval_11-May-2012.tar

下载完后解压数据集:

tar -xvf VOCtrainval_11-May-2012.tar

接着通过终端进入/xxx/models-master/research目录下建立一个sh脚本文件(例如translate_VOC.sh)复制以下代码:

python object_detection/dataset_tools/create_pascal_tf_record.py \
    --label_map_path=object_detection/data/pascal_label_map.pbtxt \
    --data_dir=/xxx/data/VOCdevkit --year=VOC2012 --set=train \
    --output_path=/xxx/data/pascal_train.record
python object_detection/dataset_tools/create_pascal_tf_record.py \
    --label_map_path=object_detection/data/pascal_label_map.pbtxt \
    --data_dir=/xxx/data/VOCdevkit --year=VOC2012 --set=val \
    --output_path=/xxx/data/pascal_val.record

这是官网提供的将PASCAL VOC数据集转为tfrecord的代码,简单讲一下,这个代码调用了两次create_pascal_tf_record.py文件分别去生成用于训练的pascal_train.record文件以及用于验证的pascal_val.record文件。在该代码中,需要改几处地方,(1)data_dir,这个是你解压的pascal voc数据集的路径(2)output_path,这个是你想要保存tfrecord文件的地方。其他的参数不要改动。保存脚本文件,在终端直接输入sh translate_VOC.sh开始转换格式,转换完成后会在相应文件夹中生成tftrecord文件。

--------------------------------------------------------------------------------

注意:pascal voc2012中训练集和验证集均提供了大约5000+张样本,如果你的电脑硬件配置不高,建议你减少验证集的图像数量,否则每次验证时会浪费你很多的时间,严重的话会导致你陷入验证的死循环,根本无法训练(官方默认每隔600秒进行一次验证)。

如何减少验证集图片数量呢?这里提供两个方法,第一个方法比较简单,第二个方法麻烦点

(1)在训练过程中通过设置SAMPLE_1_OF_N_EVAL_EXAMPLES的值来改变验证采样频率(默认为1),比如设置为5代表每五个验证样本我只抽取其中一个,简单的说就是只取样本集的五分之一进行验证,当为1时对整个验证集进行验证。

(2)打开你下载的数据集目录     /xxx/VOCdevkit/VOC2012/ImageSets/Main   ,在该目录下找到aeroplane_val.txt这个文件,打开它,大概有5000+行,每一行代表着一个验证样本,比如我只保留前500行后面的全部删掉,这样验证集就只剩500个了。

这么做的原因是,在create_pascal_tf_record.py文件的main函数中(大概在代码的165行左右)是通过aeroplane_train.txtaeroplane_val.txt这两个文件去寻找并生成数据集的。这就是为什么我们直接去处理aeroplane_val.txt文件的原因。

---------------------------------------------------------------------------------

 

2.选择并下载预训练模型

在object detection模块中,官方已经提供了很多基于coco数据集预训练好的模型,链接

下载这些模型并使用迁移学习的方式能够很快的训练出一个新的模型。在这里我们选择一个模型参数较小的ssd_mobilenet_v2_coco作为本次训练实例。直接点击模型名称即可下载,下载完成后我们将文件放到1中介绍过的文件结构的model文件夹下,并进行解压操作。

 

3.配置训练文件configuration

关于配置文件,我们能够在 models-master/research/object_detection/samples/configs/ 文件夹下找到很多配置文件模板。由于我们下载的是ssd_mobilenet_v2_coco模型,那么我们就找ssd_mobilenet_v2_coco.config文件,将这个文件复制到model文件夹下,开始进行配置。

# SSD with Mobilenet v2 configuration for MSCOCO Dataset.
# Users should configure the fine_tune_checkpoint field in the train config as
# well as the label_map_path and input_path fields in the train_input_reader and
# eval_input_reader. Search for "PATH_TO_BE_CONFIGURED" to find the fields that
# should be configured.

model {
  ssd {
    num_classes: 20
    box_coder {
      faster_rcnn_box_coder {
        y_scale: 10.0
        x_scale: 10.0
        height_scale: 5.0
        width_scale: 5.0
      }
    }
    matcher {
      argmax_matcher {
        matched_threshold: 0.5
        unmatched_threshold: 0.5
        ignore_thresholds: false
        negatives_lower_than_unmatched: true
        force_match_for_each_row: true
      }
    }
    similarity_calculator {
      iou_similarity {
      }
    }
    anchor_generator {
      ssd_anchor_generator {
        num_layers: 6
        min_scale: 0.2
        max_scale: 0.95
        aspect_ratios: 1.0
        aspect_ratios: 2.0
        aspect_ratios: 0.5
        aspect_ratios: 3.0
        aspect_ratios: 0.3333
      }
    }
    image_resizer {
      fixed_shape_resizer {
        height: 300
        width: 300
      }
    }
    box_predictor {
      convolutional_box_predictor {
        min_depth: 0
        max_depth: 0
        num_layers_before_predictor: 0
        use_dropout: false
        dropout_keep_probability: 0.8
        kernel_size: 1
        box_code_size: 4
        apply_sigmoid_to_scores: false
        conv_hyperparams {
          activation: RELU_6,
          regularizer {
            l2_regularizer {
              weight: 0.00004
            }
          }
          initializer {
            truncated_normal_initializer {
              stddev: 0.03
              mean: 0.0
            }
          }
          batch_norm {
            train: true,
            scale: true,
            center: true,
            decay: 0.9997,
            epsilon: 0.001,
          }
        }
      }
    }
    feature_extractor {
      type: 'ssd_mobilenet_v2'
      min_depth: 16
      depth_multiplier: 1.0
      conv_hyperparams {
        activation: RELU_6,
        regularizer {
          l2_regularizer {
            weight: 0.00004
          }
        }
        initializer {
          truncated_normal_initializer {
            stddev: 0.03
            mean: 0.0
          }
        }
        batch_norm {
          train: true,
          scale: true,
          center: true,
          decay: 0.9997,
          epsilon: 0.001,
        }
      }
    }
    loss {
      classification_loss {
        weighted_sigmoid {
        }
      }
      localization_loss {
        weighted_smooth_l1 {
        }
      }
      hard_example_miner {
        num_hard_examples: 3000
        iou_threshold: 0.99
        loss_type: CLASSIFICATION
        max_negatives_per_positive: 3
        min_negatives_per_image: 3
      }
      classification_weight: 1.0
      localization_weight: 1.0
    }
    normalize_loss_by_num_matches: true
    post_processing {
      batch_non_max_suppression {
        score_threshold: 1e-8
        iou_threshold: 0.6
        max_detections_per_class: 100
        max_total_detections: 100
      }
      score_converter: SIGMOID
    }
  }
}

train_config: {
  batch_size: 24
  optimizer {
    rms_prop_optimizer: {
      learning_rate: {
        exponential_decay_learning_rate {
          initial_learning_rate: 0.004
          decay_steps: 800
          decay_factor: 0.95
        }
      }
      momentum_optimizer_value: 0.9
      decay: 0.9
      epsilon: 1.0
    }
  }
  fine_tune_checkpoint: "/xxx/model/ssd_mobilenet_v2_coco_2018_03_29/model.ckpt"
  from_detection_checkpoint: true
  load_all_detection_checkpoint_vars: true
  # Note: The below line limits the training process to 200K steps, which we
  # empirically found to be sufficient enough to train the pets dataset. This
  # effectively bypasses the learning rate schedule (the learning rate will
  # never decay). Remove the below line to train indefinitely.
  num_steps: 20000
  data_augmentation_options {
    random_horizontal_flip {
    }
  }
  data_augmentation_options {
    ssd_random_crop {
    }
  }
}

train_input_reader: {
  tf_record_input_reader {
    input_path: "/xxx/data/pascal_train.record"
  }
  label_map_path: "/xxx/pascal_label_map.pbtxt"
}

eval_config: {
  num_examples: 500
  metrics_set: "coco_detection_metrics"
}

eval_input_reader: {
  tf_record_input_reader {
    input_path: "/xxx/data/pascal_val.record"
  }
  label_map_path: "/xxx/pascal_label_map.pbtxt"
  shuffle: false
  num_readers: 1
}

由于配置文件参数很多,我就讲几个只要简单改动后就能训练的参数:

1)num_classes:   这个参数就是你所训练模型的检测目标数(pascal voc是20类,所以改成20)

2)decay_steps:  这个参数的意义是每训练decay_steps步数,就进行将学习率将低一次(new_learning_rate = old_learnning_rate * decay_factor)这个根据个人训练情况改,因为这里并不是从头训练(我使用的迁移学习方法),而且我就准备迭代20000步,所以就加快降低学习率的速度(学习率过大会导致准确率一直震荡,很难收敛)。

3)fine_tune_checkpoint: 这个参数就是加载预训练模型的路径,也就是我们下载解压的ssd_mobilenet_v2_coco路径。

4)from_detection_checkpoint: 这个参数是选择加载全部模型参数true,还是只加载前置网络模型参数(分类网络)false。

5)num_steps: 20000   这个参数就是总迭代步数

6)input_path:   这个参数就是读取tfrecord文件的路径,在train_input_reader中填pascal_train.record路径,在eval_input_reader中填pascal_val.record路径。

7)label_map_path:  这个参数就是pascal_label_map.pbtxt字典文件的路径。

8)num_examples:   这个参数就是你验证集的图像数目。

9)metrics_set:   这个参数指定在验证时使用哪种评价标准,这里使用的是"coco_detection_metrics"

-----------------------------------------------------------------------------------------------

关于如何控制验证频率的问题:

在之前的版本中可以在eval_config中设置eval_interval_secs的值来改变验证之间的间隔,但现在已经失效了。官方也有说要解决这个问题,但到现在还未解决。下面给出一个控制验证间隔的方法:

打开  models-master/research/object_detection/model_lib.py文件在create_train_and_eval_specs函数中,大概750行左右的位置有如下代码:

tf.estimator.EvalSpec(
            name=eval_spec_name,
            input_fn=eval_input_fn,
            steps=None,
            exporters=exporter)

在tf.estimator.EvalSpec函数中添加如下一行(throttle_secs=3600,代表间隔1个小时验证一次,在Tensorflow文档,tf.estimator.EvalSpec函数中,默认参数start_delay_secs=120,throttle_secs=600代表默认从训练开始的第120秒开始第一次验证,之后每隔600秒验证一次):

tf.estimator.EvalSpec(
            name=eval_spec_name,
            input_fn=eval_input_fn,
            steps=None,
            exporters=exporter,
            throttle_secs=3600)

-----------------------------------------------------------------------------------------------

修改完配置文件后,我们就可以开始训练模型了。

 

4.训练模型

我们回到  models-master/research文件夹下,建立一个训练模型的脚本文件(train_VOC.sh):

PIPELINE_CONFIG_PATH=/xxx/model/ssd_mobilenet_v2_coco.config
MODEL_DIR=/xxx/model/train
NUM_TRAIN_STEPS=20000
SAMPLE_1_OF_N_EVAL_EXAMPLES=1
python object_detection/model_main.py \
    --pipeline_config_path=${PIPELINE_CONFIG_PATH} \
    --model_dir=${MODEL_DIR} \
    --num_train_steps=${NUM_TRAIN_STEPS} \
    --sample_1_of_n_eval_examples=$SAMPLE_1_OF_N_EVAL_EXAMPLES \
    --alsologtostderr

其中PIPELINE_CONFIG_PATH是我们的config配置文件路径;MODEL_DIR是训练模型的保存位置;NUM_TRAIN_STEPS是总共训练的步数;SAMPLE_1_OF_N_EVAL_EXAMPLES是验证过程中对样本的采样频率(在之前说过,假设为n则表示只对验证样本中的n分一的样本进行采样验证),若为1表示将验证所有样本,一般设为1。建立好脚本文件后通过sh train_VOC.sh开始执行。

--------------------------------------------------------------------------------

如果需要在后台运行可通过以下指令,通过该指令将终端输出信息定向输出到train.log文件中:

nohup sh train_VOC.sh > train.log 2>&1 &

通过下面指令查看log中的信息:

tail -f train.log

若需要提前终止程序,可通过查询对应程序的pid进行终止。

ps -ef | grep python  # 查找后台运行的所有pyhton程序的pid
kill -9 9208  # 终端pid为9208的程序

-----------------------------------------------------------------------------------

本人训练大概8000 step后就基本收敛了,为了节约时间,停止了训练。在coco的检测标准中IOU=0.50:0.95对应的Average Precision为0.367,IOU=0.50(IOU=0.5可以理解为pscal voc的评价标准)对应的Average Precision为0.604。

 

5.使用tensorboard查看训练过程

在训练过程中,所有的训练文件都保存在我们的train文件夹下,打开train文件夹你会发现里面有个eval_0文件夹,这个文件里保存是在整个训练过程中在验证集上的准确率变化。我们通过终端进入train文件夹,接着输入以下指令调用tensorboard:

tensorboard --logdir=eval_0/

输入指令后终端会打印出一个本地网页链接,复制该链接,打开你的浏览器输入该链接就能看到整个过程的训练结果。在SCALARS栏中有DetectionBoxes_Precision、DetectionBoxes_Pecall、Loss、leaning_rate等曲线,在IMAGES栏中有使用模型在验证集上预测的图像,左侧是预测的结果右侧是标准检测结果。GRAPHS是整个训练的流程图,太乱了看不懂。

 

6.冻结模型参数

模型训练完成后所有的参数是以model.ckpt.data、model.ckpt.meta和model.ckpt.index三个文件共同保存,在我们使用的ssd_mobilenet_v2模型中,模型ckpt文件大小约78M,如果将模型参数冻结后只有20M。当然,不论是ckpt文件还是冻结后的pb文件我们都能够进行调用,但ckpt文件更适合在训练模型等实验过程中使用,而pb文件是根据我们需要的输出节点反向查找将ckpt中不需要的节点全部删除后获得的文件(还将模型参数的保存形式进行了改变)由于该文件占用内存更小,更适合部署在你所需要使用的设备上。(tensorflow还有很多方法能够将模型的大小进一步缩减占用更少的内存)

我们直接进入 models-master/research/ 文件夹下,建立冻结模型权重脚本(export_Pb.sh):

INPUT_TYPE=image_tensor
PIPELINE_CONFIG_PATH=/xxx/model/ssd_mobilenet_v2_coco.config
TRAINED_CKPT_PREFIX=/xxx/model/train/model.ckpt-8000
EXPORT_DIR=/xxx/model/eval/
python object_detection/export_inference_graph.py \
    --input_type=${INPUT_TYPE} \
    --pipeline_config_path=${PIPELINE_CONFIG_PATH} \
    --trained_checkpoint_prefix=${TRAINED_CKPT_PREFIX} \
    --output_directory=${EXPORT_DIR}

PIPELINE_CONFIG_PATH就是模型的config配置文件路径;TRAINED_CKPT_PREFIX是训练模型的ckpt权重最后面的数字对应的训练步数(一般选择最后保存的一个ckpt就行);EXPORT_DIR就是冻结模型pb的输出位置。执行脚本后在/xxx/model/eval/目录下就会生成我们所需要的pb文件(frozen_inference_graph.pb)。

 

7.调用pb文件进行预测

我们新建一个python项目,将 models-master/research/目录下的object_detection 整个文件夹复制到我们的python项目中,接着将之前生成的pb文件所在文件夹eval复制到我们的项目中,接着将我们的标签文件pascal_lable_map.pbtxt文件复制到项目中,接着检查下object_detection文件夹下的test_images文件夹中是否有两张图片(一张是两个狗狗,一张是object detection官网上用的那个海边图片),后面需要使用,目录结构如下。

+pythonProject
   +object_detection
      +test_images
   +eval
   -pascal_label_map.pbtxt
   -main.py

创建一个python文件main.py,将下面代码复制到py文件中,该代码是根据官网的object_detection_tutorial.ipynb教程简单修改得到的。代码中也有注释,便于理解。

 

import numpy as np
import os
import glob
import tensorflow as tf

from distutils.version import StrictVersion
import matplotlib
from matplotlib import pyplot as plt
from PIL import Image


from object_detection.utils import ops as utils_ops

from object_detection.utils import label_map_util

from object_detection.utils import visualization_utils as vis_util

# 防止backend='Agg'导致不显示图像的情况
matplotlib.use('TkAgg')

if StrictVersion(tf.__version__) < StrictVersion('1.12.0'):
    raise ImportError('Please upgrade your TensorFlow installation to v1.12.*.')

MODEL_NAME = 'eval'

# Path to frozen detection graph. This is the actual model that is used for the object detection.
PATH_TO_FROZEN_GRAPH = MODEL_NAME + '/frozen_inference_graph.pb'

# List of the strings that is used to add correct label for each box.
PATH_TO_LABELS = 'pascal_label_map.pbtxt'

detection_graph = tf.Graph()
with detection_graph.as_default():
    od_graph_def = tf.GraphDef()
    with tf.gfile.GFile(PATH_TO_FROZEN_GRAPH, 'rb') as fid:
        serialized_graph = fid.read()
        od_graph_def.ParseFromString(serialized_graph)
        tf.import_graph_def(od_graph_def, name='')

category_index = label_map_util.create_category_index_from_labelmap(PATH_TO_LABELS, use_display_name=True)

def load_image_into_numpy_array(image):
    im_width, im_height = image.size
    return np.array(image.getdata()).reshape(
        (im_height, im_width, 3)).astype(np.uint8)

# For the sake of simplicity we will use only 2 images:
# image1.jpg
# image2.jpg
# If you want to test the code with your images, just add path to the images to the TEST_IMAGE_PATHS.
PATH_TO_TEST_IMAGES_DIR = 'object_detection/test_images/*.jpg'
TEST_IMAGE_PATHS = glob.glob(PATH_TO_TEST_IMAGES_DIR)

# Size, in inches, of the output images.
IMAGE_SIZE = (12, 8)

def run_inference_for_single_image(image, graph):
    with graph.as_default():
        with tf.Session() as sess:
            # Get handles to input and output tensors
            ops = tf.get_default_graph().get_operations()
            all_tensor_names = {output.name for op in ops for output in op.outputs}
            tensor_dict = {}
            for key in [
                'num_detections', 'detection_boxes', 'detection_scores',
                'detection_classes', 'detection_masks'
            ]:
                tensor_name = key + ':0'
                if tensor_name in all_tensor_names:
                    tensor_dict[key] = tf.get_default_graph().get_tensor_by_name(
                        tensor_name)
            if 'detection_masks' in tensor_dict:
                # The following processing is only for single image
                detection_boxes = tf.squeeze(tensor_dict['detection_boxes'], [0])
                detection_masks = tf.squeeze(tensor_dict['detection_masks'], [0])
                # Reframe is required to translate mask from box coordinates to image coordinates and fit the image size.
                real_num_detection = tf.cast(tensor_dict['num_detections'][0], tf.int32)
                detection_boxes = tf.slice(detection_boxes, [0, 0], [real_num_detection, -1])
                detection_masks = tf.slice(detection_masks, [0, 0, 0], [real_num_detection, -1, -1])
                detection_masks_reframed = utils_ops.reframe_box_masks_to_image_masks(
                    detection_masks, detection_boxes, image.shape[1], image.shape[2])
                detection_masks_reframed = tf.cast(
                    tf.greater(detection_masks_reframed, 0.5), tf.uint8)
                # Follow the convention by adding back the batch dimension
                tensor_dict['detection_masks'] = tf.expand_dims(
                    detection_masks_reframed, 0)
            image_tensor = tf.get_default_graph().get_tensor_by_name('image_tensor:0')

            # Run inference
            output_dict = sess.run(tensor_dict,
                                   feed_dict={image_tensor: image})

            # all outputs are float32 numpy arrays, so convert types as appropriate
            output_dict['num_detections'] = int(output_dict['num_detections'][0])
            output_dict['detection_classes'] = output_dict[
                'detection_classes'][0].astype(np.int64)
            output_dict['detection_boxes'] = output_dict['detection_boxes'][0]
            output_dict['detection_scores'] = output_dict['detection_scores'][0]
            if 'detection_masks' in output_dict:
                output_dict['detection_masks'] = output_dict['detection_masks'][0]
    return output_dict

for image_path in TEST_IMAGE_PATHS:
    image = Image.open(image_path)
    # the array based representation of the image will be used later in order to prepare the
    # result image with boxes and labels on it.
    image_np = load_image_into_numpy_array(image)
    # Expand dimensions since the model expects images to have shape: [1, None, None, 3]
    image_np_expanded = np.expand_dims(image_np, axis=0)
    # Actual detection.
    output_dict = run_inference_for_single_image(image_np_expanded, detection_graph)
    # Visualization of the results of a detection.
    vis_util.visualize_boxes_and_labels_on_image_array(
        image_np,
        output_dict['detection_boxes'],
        output_dict['detection_classes'],
        output_dict['detection_scores'],
        category_index,
        instance_masks=output_dict.get('detection_masks'),
        use_normalized_coordinates=True,
        line_thickness=8)
    plt.figure(figsize=IMAGE_SIZE)
    plt.imshow(image_np)
    plt.show()

大家可以自己多找几张图片放到test_images文件夹下试试效果,是不是很有成就感,撒花✿✿ヽ(°▽°)ノ✿    

在下一篇博客中将会介绍如何使用自己的数据集去训练模型。

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章