python目標(biāo)檢測(cè)SSD算法訓(xùn)練部分源碼詳解
學(xué)習(xí)前言
……又看了很久的SSD算法,今天講解一下訓(xùn)練部分的代碼。
預(yù)測(cè)部分的代碼可以參照http://www.dbjr.com.cn/article/246905.htm
講解構(gòu)架
本次教程的講解主要是對(duì)訓(xùn)練部分的代碼進(jìn)行講解,該部分講解主要是對(duì)訓(xùn)練函數(shù)的執(zhí)行過(guò)程與執(zhí)行思路進(jìn)行詳解。
訓(xùn)練函數(shù)的執(zhí)行過(guò)程大體上分為:
1、設(shè)定訓(xùn)練參數(shù)。
2、讀取數(shù)據(jù)集。
3、建立ssd網(wǎng)絡(luò)。
4、預(yù)處理數(shù)據(jù)集。
5、對(duì)ground truth實(shí)際框進(jìn)行編碼,使其格式符合神經(jīng)網(wǎng)絡(luò)的預(yù)測(cè)結(jié)果,便于比較。
6、計(jì)算loss值。
7、利用優(yōu)化器完成梯度下降并保存模型。
在看本次算法前,建議先下載我簡(jiǎn)化過(guò)的源碼,配合觀看,具體運(yùn)行方法在開(kāi)始訓(xùn)練部分
下載鏈接 https://pan.baidu.com/s/1K4RAJvLj11blywuX2CrLSA
提取碼:4wbi
模型訓(xùn)練的流程
本文使用的ssd_vgg_300的源碼點(diǎn)擊下載,本文對(duì)其進(jìn)行了簡(jiǎn)化,保留了上一次篩選出的預(yù)測(cè)部分,還加入了訓(xùn)練部分,便于理順整個(gè)SSD的框架。
1、設(shè)置參數(shù)
在載入數(shù)據(jù)庫(kù)前,首先要設(shè)定一系列的參數(shù),這些參數(shù)可以分為幾個(gè)部分。第一部分是SSD網(wǎng)絡(luò)中的一些標(biāo)志參數(shù):
# =========================================================================== #
# SSD Network flags.
# =========================================================================== #
# localization框的衰減比率
tf.app.flags.DEFINE_float(
'loss_alpha', 1., 'Alpha parameter in the loss function.')
# 正負(fù)樣本比率
tf.app.flags.DEFINE_float(
'negative_ratio', 3., 'Negative ratio in the loss function.')
# ground truth處理后,匹配得分高于match_threshold屬于正樣本
tf.app.flags.DEFINE_float(
'match_threshold', 0.5, 'Matching threshold in the loss function.')
第二部分是訓(xùn)練時(shí)的參數(shù)(包括訓(xùn)練效果輸出、保存方案等):
# =========================================================================== #
# General Flags.
# =========================================================================== #
# train_dir用于保存訓(xùn)練后的模型和日志
tf.app.flags.DEFINE_string(
'train_dir', '/tmp/tfmodel/',
'Directory where checkpoints and event logs are written to.')
# num_readers是在對(duì)數(shù)據(jù)集進(jìn)行讀取時(shí)所用的平行讀取器個(gè)數(shù)
tf.app.flags.DEFINE_integer(
'num_readers', 4,
'The number of parallel readers that read data from the dataset.')
# 在進(jìn)行訓(xùn)練batch的構(gòu)建時(shí),所用的線程數(shù)
tf.app.flags.DEFINE_integer(
'num_preprocessing_threads', 4,
'The number of threads used to create the batches.')
# 每十步進(jìn)行一次log輸出,在窗口上
tf.app.flags.DEFINE_integer(
'log_every_n_steps', 10,
'The frequency with which logs are print.')
# 每600秒存儲(chǔ)一次記錄
tf.app.flags.DEFINE_integer(
'save_summaries_secs', 600,
'The frequency with which summaries are saved, in seconds.')
# 每600秒存儲(chǔ)一次模型
tf.app.flags.DEFINE_integer(
'save_interval_secs', 600,
'The frequency with which the model is saved, in seconds.')
# 可以使用的gpu內(nèi)存數(shù)量
tf.app.flags.DEFINE_float(
'gpu_memory_fraction', 0.7, 'GPU memory fraction to use.')
第三部分是優(yōu)化器參數(shù):
# =========================================================================== #
# Optimization Flags.
# =========================================================================== #
# 優(yōu)化器參數(shù)
# weight_decay參數(shù)
tf.app.flags.DEFINE_float(
'weight_decay', 0.00004, 'The weight decay on the model weights.')
# 使用什么優(yōu)化器
tf.app.flags.DEFINE_string(
'optimizer', 'rmsprop',
'The name of the optimizer, one of "adadelta", "adagrad", "adam",'
'"ftrl", "momentum", "sgd" or "rmsprop".')
tf.app.flags.DEFINE_float(
'adadelta_rho', 0.95,
'The decay rate for adadelta.')
tf.app.flags.DEFINE_float(
'adagrad_initial_accumulator_value', 0.1,
'Starting value for the AdaGrad accumulators.')
tf.app.flags.DEFINE_float(
'adam_beta1', 0.9,
'The exponential decay rate for the 1st moment estimates.')
tf.app.flags.DEFINE_float(
'adam_beta2', 0.999,
'The exponential decay rate for the 2nd moment estimates.')
tf.app.flags.DEFINE_float('opt_epsilon', 1.0, 'Epsilon term for the optimizer.')
tf.app.flags.DEFINE_float('ftrl_learning_rate_power', -0.5,
'The learning rate power.')
tf.app.flags.DEFINE_float(
'ftrl_initial_accumulator_value', 0.1,
'Starting value for the FTRL accumulators.')
tf.app.flags.DEFINE_float(
'ftrl_l1', 0.0, 'The FTRL l1 regularization strength.')
tf.app.flags.DEFINE_float(
'ftrl_l2', 0.0, 'The FTRL l2 regularization strength.')
tf.app.flags.DEFINE_float(
'momentum', 0.9,
'The momentum for the MomentumOptimizer and RMSPropOptimizer.')
tf.app.flags.DEFINE_float('rmsprop_momentum', 0.9, 'Momentum.')
tf.app.flags.DEFINE_float('rmsprop_decay', 0.9, 'Decay term for RMSProp.')
第四部分是學(xué)習(xí)率參數(shù):
# =========================================================================== #
# Learning Rate Flags.
# =========================================================================== #
# 學(xué)習(xí)率衰減的方式,有固定、指數(shù)衰減等
tf.app.flags.DEFINE_string(
'learning_rate_decay_type',
'exponential',
'Specifies how the learning rate is decayed. One of "fixed", "exponential",'
' or "polynomial"')
# 初始學(xué)習(xí)率
tf.app.flags.DEFINE_float('learning_rate', 0.01, 'Initial learning rate.')
# 結(jié)束時(shí)的學(xué)習(xí)率
tf.app.flags.DEFINE_float(
'end_learning_rate', 0.0001,
'The minimal end learning rate used by a polynomial decay learning rate.')
tf.app.flags.DEFINE_float(
'label_smoothing', 0.0, 'The amount of label smoothing.')
# 學(xué)習(xí)率衰減因素
tf.app.flags.DEFINE_float(
'learning_rate_decay_factor', 0.94, 'Learning rate decay factor.')
tf.app.flags.DEFINE_float(
'num_epochs_per_decay', 2.0,
'Number of epochs after which learning rate decays.')
tf.app.flags.DEFINE_float(
'moving_average_decay', None,
'The decay to use for the moving average.'
'If left as None, then moving averages are not used.')
第五部分是數(shù)據(jù)集參數(shù):
# =========================================================================== #
# Dataset Flags.
# =========================================================================== #
# 數(shù)據(jù)集名稱
tf.app.flags.DEFINE_string(
'dataset_name', 'imagenet', 'The name of the dataset to load.')
# 數(shù)據(jù)集種類(lèi)個(gè)數(shù)
tf.app.flags.DEFINE_integer(
'num_classes', 21, 'Number of classes to use in the dataset.')
# 訓(xùn)練還是測(cè)試
tf.app.flags.DEFINE_string(
'dataset_split_name', 'train', 'The name of the train/test split.')
# 數(shù)據(jù)集目錄
tf.app.flags.DEFINE_string(
'dataset_dir', None, 'The directory where the dataset files are stored.')
tf.app.flags.DEFINE_integer(
'labels_offset', 0,
'An offset for the labels in the dataset. This flag is primarily used to '
'evaluate the VGG and ResNet architectures which do not use a background '
'class for the ImageNet dataset.')
tf.app.flags.DEFINE_string(
'model_name', 'ssd_300_vgg', 'The name of the architecture to train.')
tf.app.flags.DEFINE_string(
'preprocessing_name', None, 'The name of the preprocessing to use. If left '
'as `None`, then the model_name flag is used.')
# 每一次訓(xùn)練batch的大小
tf.app.flags.DEFINE_integer(
'batch_size', 32, 'The number of samples in each batch.')
# 訓(xùn)練圖片的大小
tf.app.flags.DEFINE_integer(
'train_image_size', None, 'Train image size')
# 最大訓(xùn)練次數(shù)
tf.app.flags.DEFINE_integer('max_number_of_steps', 50000,
'The maximum number of training steps.')
第六部分是微修已有的模型所需的參數(shù):
# =========================================================================== #
# Fine-Tuning Flags.
# =========================================================================== #
# 該部分參數(shù)用于微修已有的模型
# 原模型的位置
tf.app.flags.DEFINE_string(
'checkpoint_path', None,
'The path to a checkpoint from which to fine-tune.')
tf.app.flags.DEFINE_string(
'checkpoint_model_scope', None,
'Model scope in the checkpoint. None if the same as the trained model.')
# 哪些變量不要
tf.app.flags.DEFINE_string(
'checkpoint_exclude_scopes', None,
'Comma-separated list of scopes of variables to exclude when restoring '
'from a checkpoint.')
# 那些變量不訓(xùn)練
tf.app.flags.DEFINE_string(
'trainable_scopes', None,
'Comma-separated list of scopes to filter the set of variables to train.'
'By default, None would train all the variables.')
# 忽略丟失的變量
tf.app.flags.DEFINE_boolean(
'ignore_missing_vars', False,
'When restoring a checkpoint would ignore missing variables.')
FLAGS = tf.app.flags.FLAGS
所有的參數(shù)的意義我都進(jìn)行了標(biāo)注,在實(shí)際訓(xùn)練的時(shí)候需要修改一些參數(shù)的內(nèi)容,這些參數(shù)看起來(lái)多,其實(shí)只是包含了一個(gè)網(wǎng)絡(luò)訓(xùn)練所有必須的部分:
- 網(wǎng)絡(luò)主體參數(shù);
- 訓(xùn)練時(shí)的普通參數(shù)(包括訓(xùn)練效果輸出、保存方案等);
- 優(yōu)化器參數(shù);
- 學(xué)習(xí)率參數(shù);
- 數(shù)據(jù)集參數(shù);
- 微修已有的模型的參數(shù)設(shè)置。
2、讀取數(shù)據(jù)集
在訓(xùn)練流程中,其通過(guò)如下函數(shù)讀取數(shù)據(jù)集
##########################讀取數(shù)據(jù)集部分#############################
# 選擇數(shù)據(jù)庫(kù)
dataset = dataset_factory.get_dataset(
FLAGS.dataset_name, FLAGS.dataset_split_name, FLAGS.dataset_dir)
dataset_factory里面放的是數(shù)據(jù)集獲取和處理的函數(shù),這里面對(duì)應(yīng)了4個(gè)數(shù)據(jù)集, 利用datasets_map存儲(chǔ)了四個(gè)數(shù)據(jù)集的處理代碼。
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from datasets import cifar10
from datasets import imagenet
from datasets import pascalvoc_2007
from datasets import pascalvoc_2012
datasets_map = {
'cifar10': cifar10,
'imagenet': imagenet,
'pascalvoc_2007': pascalvoc_2007,
'pascalvoc_2012': pascalvoc_2012,
}
def get_dataset(name, split_name, dataset_dir, file_pattern=None, reader=None):
"""
給定一個(gè)數(shù)據(jù)集名和一個(gè)拆分名返回一個(gè)數(shù)據(jù)集。
參數(shù):
name: String, 數(shù)據(jù)集名稱
split_name: 訓(xùn)練還是測(cè)試
dataset_dir: 存儲(chǔ)數(shù)據(jù)集文件的目錄。
file_pattern: 用于匹配數(shù)據(jù)集源文件的文件模式。
reader: tf.readerbase的子類(lèi)。如果保留為“none”,則使用每個(gè)數(shù)據(jù)集定義的默認(rèn)讀取器。
Returns:
數(shù)據(jù)集
"""
if name not in datasets_map:
raise ValueError('Name of dataset unknown %s' % name)
return datasets_map[name].get_split(split_name,
dataset_dir,
file_pattern,
reader)
我們這里用到pascalvoc_2012的數(shù)據(jù),所以當(dāng)返回datasets_map[name].get_split這個(gè)代碼時(shí),實(shí)際上調(diào)用的是:
pascalvoc_2012.get_split(split_name, dataset_dir, file_pattern, reader)
在pascalvoc_2012中g(shù)et_split的執(zhí)行過(guò)程如下,其中file_pattern = ‘voc_2012_%s_*.tfrecord’,這個(gè)名稱是訓(xùn)練的圖片的默認(rèn)名稱,實(shí)際訓(xùn)練的tfrecord文件名稱像這樣voc_2012_train_001.tfrecord,意味著可以讀取這樣的訓(xùn)練文件:
def get_split(split_name, dataset_dir, file_pattern=None, reader=None):
"""Gets a dataset tuple with instructions for reading ImageNet.
Args:
split_name: 訓(xùn)練還是測(cè)試
dataset_dir: 數(shù)據(jù)集的位置
file_pattern: 匹配數(shù)據(jù)集源時(shí)要使用的文件模式。
假定模式包含一個(gè)'%s'字符串,以便可以插入拆分名稱
reader: TensorFlow閱讀器類(lèi)型。
Returns:
數(shù)據(jù)集.
"""
if not file_pattern:
file_pattern = FILE_PATTERN
return pascalvoc_common.get_split(split_name, dataset_dir,
file_pattern, reader,
SPLITS_TO_SIZES,
ITEMS_TO_DESCRIPTIONS,
NUM_CLASSES)
再進(jìn)入到pascalvoc_common文件后,實(shí)際上就開(kāi)始對(duì)tfrecord的文件進(jìn)行分割了,通過(guò)代碼注釋我們了解代碼的執(zhí)行過(guò)程,其中tfrecord的文件讀取就是首先按照keys_to_features的內(nèi)容進(jìn)行文件解碼,解碼后的結(jié)果按照items_to_handlers的格式存入數(shù)據(jù)集:
def get_split(split_name, dataset_dir, file_pattern, reader,
split_to_sizes, items_to_descriptions, num_classes):
"""Gets a dataset tuple with instructions for reading Pascal VOC dataset.
給定一個(gè)數(shù)據(jù)集名和一個(gè)拆分名返回一個(gè)數(shù)據(jù)集。
參數(shù):
name: String, 數(shù)據(jù)集名稱
split_name: 訓(xùn)練還是測(cè)試
dataset_dir: 存儲(chǔ)數(shù)據(jù)集文件的目錄。
file_pattern: 用于匹配數(shù)據(jù)集源文件的文件模式。
reader: tf.readerbase的子類(lèi)。如果保留為“none”,則使用每個(gè)數(shù)據(jù)集定義的默認(rèn)讀取器。
Returns:
數(shù)據(jù)集
"""
if split_name not in split_to_sizes:
raise ValueError('split name %s was not recognized.' % split_name)
# file_pattern是取得的tfrecord數(shù)據(jù)集的位置
file_pattern = os.path.join(dataset_dir, file_pattern % split_name)
# 當(dāng)沒(méi)有的時(shí)候使用默認(rèn)reader
if reader is None:
reader = tf.TFRecordReader
# VOC數(shù)據(jù)集中的文檔內(nèi)容
keys_to_features = {
'image/encoded': tf.FixedLenFeature((), tf.string, default_value=''),
'image/format': tf.FixedLenFeature((), tf.string, default_value='jpeg'),
'image/height': tf.FixedLenFeature([1], tf.int64),
'image/width': tf.FixedLenFeature([1], tf.int64),
'image/channels': tf.FixedLenFeature([1], tf.int64),
'image/shape': tf.FixedLenFeature([3], tf.int64),
'image/object/bbox/xmin': tf.VarLenFeature(dtype=tf.float32),
'image/object/bbox/ymin': tf.VarLenFeature(dtype=tf.float32),
'image/object/bbox/xmax': tf.VarLenFeature(dtype=tf.float32),
'image/object/bbox/ymax': tf.VarLenFeature(dtype=tf.float32),
'image/object/bbox/label': tf.VarLenFeature(dtype=tf.int64),
'image/object/bbox/difficult': tf.VarLenFeature(dtype=tf.int64),
'image/object/bbox/truncated': tf.VarLenFeature(dtype=tf.int64),
}
# 解碼方式
items_to_handlers = {
'image': slim.tfexample_decoder.Image('image/encoded', 'image/format'),
'shape': slim.tfexample_decoder.Tensor('image/shape'),
'object/bbox': slim.tfexample_decoder.BoundingBox(
['ymin', 'xmin', 'ymax', 'xmax'], 'image/object/bbox/'),
'object/label': slim.tfexample_decoder.Tensor('image/object/bbox/label'),
'object/difficult': slim.tfexample_decoder.Tensor('image/object/bbox/difficult'),
'object/truncated': slim.tfexample_decoder.Tensor('image/object/bbox/truncated'),
}
# 將tfrecord上keys_to_features的部分解碼到items_to_handlers上
decoder = slim.tfexample_decoder.TFExampleDecoder(
keys_to_features, items_to_handlers)
labels_to_names = None
if dataset_utils.has_labels(dataset_dir):
labels_to_names = dataset_utils.read_label_file(dataset_dir)
return slim.dataset.Dataset(
data_sources=file_pattern, # 數(shù)據(jù)源
reader=reader, # tf.TFRecordReader
decoder=decoder, # 解碼結(jié)果
num_samples=split_to_sizes[split_name], # 17125
items_to_descriptions=items_to_descriptions, # 每一個(gè)item的描述
num_classes=num_classes, # 種類(lèi)
labels_to_names=labels_to_names)
通過(guò)上述一系列操作,實(shí)際上是返回了一個(gè)slim.dataset.Dataset數(shù)據(jù)集,而一系列函數(shù)的調(diào)用,實(shí)際上是為了調(diào)用對(duì)應(yīng)的數(shù)據(jù)集。
3、建立ssd網(wǎng)絡(luò)。
建立ssd網(wǎng)絡(luò)的過(guò)程并不復(fù)雜,沒(méi)有許多函數(shù)的調(diào)用,實(shí)際執(zhí)行過(guò)程如果了解ssd網(wǎng)絡(luò)的預(yù)測(cè)部分就很好理解,我這里只講下邏輯:
1、利用ssd_class = ssd_vgg_300.SSDNet獲得SSDNet的類(lèi)
2、替換種類(lèi)的數(shù)量num_classes參數(shù)
3、利用ssd_net = ssd_class(ssd_params)建立網(wǎng)絡(luò)
4、獲得先驗(yàn)框
調(diào)用的代碼如下:
###########################建立ssd網(wǎng)絡(luò)############################## # 獲得SSD的網(wǎng)絡(luò)和它的先驗(yàn)框 ssd_class = ssd_vgg_300.SSDNet # 替換種類(lèi)的數(shù)量num_classes參數(shù) ssd_params = ssd_class.default_params._replace(num_classes=FLAGS.num_classes) # 成功建立了網(wǎng)絡(luò)net,替換參數(shù) ssd_net = ssd_class(ssd_params) # 獲得先驗(yàn)框 ssd_shape = ssd_net.params.img_shape ssd_anchors = ssd_net.anchors(ssd_shape) # 包括六個(gè)特征層的先驗(yàn)框
4、預(yù)處理數(shù)據(jù)集
預(yù)處理數(shù)據(jù)集的代碼比較長(zhǎng),但是邏輯并不難理解。
1、獲得數(shù)據(jù)集名稱。
2、獲取數(shù)據(jù)集處理的函數(shù)。
3、利用DatasetDataProviders從數(shù)據(jù)集中提供數(shù)據(jù),進(jìn)行數(shù)據(jù)的預(yù)加載。
4、獲取原始的圖片和它對(duì)應(yīng)的label,框ground truth的位置
5、預(yù)處理圖片標(biāo)簽和框的位置
具體實(shí)現(xiàn)的代碼如下:
###########################預(yù)處理數(shù)據(jù)集##############################
# preprocessing_name等于ssd_300_vgg
preprocessing_name = FLAGS.preprocessing_name or FLAGS.model_name
# 根據(jù)名字進(jìn)行處理獲得處理函數(shù)
image_preprocessing_fn = preprocessing_factory.get_preprocessing(
preprocessing_name, is_training=True)
# 打印參數(shù)
tf_utils.print_configuration(FLAGS.__flags, ssd_params,
dataset.data_sources, FLAGS.train_dir)
# DatasetDataProviders從數(shù)據(jù)集中提供數(shù)據(jù). 通過(guò)配置,
# 可以同時(shí)使用多個(gè)readers或者使用單個(gè)reader提供數(shù)據(jù)。此外,被讀取的數(shù)據(jù)
# 可以被打亂順序
# 預(yù)加載
with tf.name_scope(FLAGS.dataset_name + '_data_provider'):
provider = slim.dataset_data_provider.DatasetDataProvider(
dataset,
num_readers=FLAGS.num_readers,
common_queue_capacity=20 * FLAGS.batch_size,
common_queue_min=10 * FLAGS.batch_size,
shuffle=True)
# 獲取原始的圖片和它對(duì)應(yīng)的label,框ground truth的位置
[image, _, glabels, gbboxes] = provider.get(['image', 'shape',
'object/label',
'object/bbox'])
# 預(yù)處理圖片標(biāo)簽和框的位置
image, glabels, gbboxes = \
image_preprocessing_fn(image, glabels, gbboxes,
out_shape=ssd_shape,
data_format=DATA_FORMAT)
在這一部分中,可能存在的疑惑的是第二步和第五步,實(shí)際上第五步調(diào)用的就是第二步中的圖像預(yù)處理函數(shù),所以我們只要看懂第二步“獲取數(shù)據(jù)集處理的函數(shù)“即可。
獲得處理函數(shù)的代碼是:
# 根據(jù)名字進(jìn)行處理獲得處理函數(shù)
image_preprocessing_fn = preprocessing_factory.get_preprocessing(
preprocessing_name, is_training=True)
preprocessing_factory的文件夾內(nèi)存放的都是圖片處理的代碼,在進(jìn)入到get_preprocessing方法后,實(shí)際上會(huì)返回一個(gè)preprocessing_fn函數(shù)。
該函數(shù)的作用實(shí)際上是返回ssd_vgg_preprocessing.preprocess_image處理后的結(jié)果。
而ssd_vgg_preprocessing.preprocess_image實(shí)際上是preprocess_for_train處理后的結(jié)果。
preprocessing_factory的get_preprocessing代碼如下:
def get_preprocessing(name, is_training=False):
preprocessing_fn_map = {
'ssd_300_vgg': ssd_vgg_preprocessing
}
if name not in preprocessing_fn_map:
raise ValueError('Preprocessing name [%s] was not recognized' % name)
def preprocessing_fn(image, labels, bboxes,
out_shape, data_format='NHWC', **kwargs):
# 這里實(shí)際上調(diào)用ssd_vgg_preprocessing.preprocess_image
return preprocessing_fn_map[name].preprocess_image(
image, labels, bboxes, out_shape, data_format=data_format,
is_training=is_training, **kwargs)
return preprocessing_fn
ssd_vgg_preprocessing的preprocess_image代碼如下:
def preprocess_image(image,
labels,
bboxes,
out_shape,
data_format,
is_training=False,
**kwargs):
"""Pre-process an given image.
Args:
image: A `Tensor` representing an image of arbitrary size.
output_height: 預(yù)處理后圖像的高度。
output_width: 預(yù)處理后圖像的寬度。
is_training: 如果我們正在對(duì)圖像進(jìn)行預(yù)處理以進(jìn)行訓(xùn)練,則為true;否則為false
resize_side_min: 圖像最小邊的下界,用于保持方向的大小調(diào)整,
如果“is_training”為“false”,則此值
用于重新縮放
resize_side_max: 圖像最小邊的上界,用于保持方向的大小調(diào)整
如果“is_training”為“false”,則此值
用于重新縮放
the resize side is sampled from
[resize_size_min, resize_size_max].
Returns:
預(yù)處理后的圖片
"""
if is_training:
return preprocess_for_train(image, labels, bboxes,
out_shape=out_shape,
data_format=data_format)
else:
return preprocess_for_eval(image, labels, bboxes,
out_shape=out_shape,
data_format=data_format,
**kwargs)
實(shí)際上最終是通過(guò)preprocess_for_train處理數(shù)據(jù)集。
preprocess_for_train處理的過(guò)程是:
1、改變數(shù)據(jù)類(lèi)型。
2、樣本框扭曲。
3、將圖像大小調(diào)整為輸出大小。
4、隨機(jī)水平翻轉(zhuǎn)圖像。
5、隨機(jī)扭曲顏色。有四種方法。
6、圖像減去平均值
執(zhí)行代碼如下:
def preprocess_for_train(image, labels, bboxes,
out_shape, data_format='NHWC',
scope='ssd_preprocessing_train'):
"""Preprocesses the given image for training.
Note that the actual resizing scale is sampled from
[`resize_size_min`, `resize_size_max`].
參數(shù):
image: 圖片,任意size的圖片.
output_height: 處理后的圖片高度.
output_width: 處理后的圖片寬度.
resize_side_min: 圖像最小邊的下界,用于保方面調(diào)整大小
resize_side_max: 圖像最小邊的上界,用于保方面調(diào)整大小
Returns:
處理過(guò)的圖片
"""
fast_mode = False
with tf.name_scope(scope, 'ssd_preprocessing_train', [image, labels, bboxes]):
if image.get_shape().ndims != 3:
raise ValueError('Input must be of size [height, width, C>0]')
# 改變圖片的數(shù)據(jù)類(lèi)型
if image.dtype != tf.float32:
image = tf.image.convert_image_dtype(image, dtype=tf.float32)
# 樣本框扭曲
dst_image = image
dst_image, labels, bboxes, _ = \
distorted_bounding_box_crop(image, labels, bboxes,
min_object_covered=MIN_OBJECT_COVERED,
aspect_ratio_range=CROP_RATIO_RANGE)
# 將圖像大小調(diào)整為輸出大小。
dst_image = tf_image.resize_image(dst_image, out_shape,
method=tf.image.ResizeMethod.BILINEAR,
align_corners=False)
# 隨機(jī)水平翻轉(zhuǎn)圖像.
dst_image, bboxes = tf_image.random_flip_left_right(dst_image, bboxes)
# 隨機(jī)扭曲顏色。有四種方法.
dst_image = apply_with_random_selector(
dst_image,
lambda x, ordering: distort_color(x, ordering, fast_mode),
num_cases=4)
# 圖像減去平均值
image = dst_image * 255.
image = tf_image_whitened(image, [_R_MEAN, _G_MEAN, _B_MEAN])
# 圖像的類(lèi)型
if data_format == 'NCHW':
image = tf.transpose(image, perm=(2, 0, 1))
return image, labels, bboxes
5、框的編碼
該部分利用如下代碼調(diào)用框的編碼代碼:
gclasses, glocalisations, gscores = ssd_net.bboxes_encode(glabels, gbboxes, ssd_anchors)
實(shí)際上bboxes_encode方法中,調(diào)用的是ssd_common模塊中的tf_ssd_bboxes_encode。
def bboxes_encode(self, labels, bboxes, anchors,
scope=None):
"""
進(jìn)行編碼操作
"""
return ssd_common.tf_ssd_bboxes_encode(
labels, bboxes, anchors,
self.params.num_classes,
self.params.no_annotation_label,
ignore_threshold=0.5,
prior_scaling=self.params.prior_scaling,
scope=scope)
ssd_common.tf_ssd_bboxes_encode執(zhí)行的代碼是對(duì)特征層每一層進(jìn)行編碼操作。
def tf_ssd_bboxes_encode(labels,
bboxes,
anchors,
num_classes,
no_annotation_label,
ignore_threshold=0.5,
prior_scaling=[0.1, 0.1, 0.2, 0.2],
dtype=tf.float32,
scope='ssd_bboxes_encode'):
"""
對(duì)每一個(gè)特征層進(jìn)行解碼
"""
with tf.name_scope(scope):
target_labels = []
target_localizations = []
target_scores = []
for i, anchors_layer in enumerate(anchors):
with tf.name_scope('bboxes_encode_block_%i' % i):
t_labels, t_loc, t_scores = \
tf_ssd_bboxes_encode_layer(labels, bboxes, anchors_layer,
num_classes, no_annotation_label,
ignore_threshold,
prior_scaling, dtype)
target_labels.append(t_labels)
target_localizations.append(t_loc)
target_scores.append(t_scores)
return target_labels, target_localizations, target_scores
實(shí)際上具體解碼的操作在函數(shù)tf_ssd_bboxes_encode_layer里,tf_ssd_bboxes_encode_layer解碼的思路是:
1、創(chuàng)建一系列變量用于存儲(chǔ)編碼結(jié)果。
yref, xref, href, wref = anchors_layer
ymin = yref - href / 2.
xmin = xref - wref / 2.
ymax = yref + href / 2.
xmax = xref + wref / 2.
vol_anchors = (xmax - xmin) * (ymax - ymin)
# 1、創(chuàng)建一系列變量存儲(chǔ)編碼結(jié)果
# 每個(gè)特征層的shape
shape = (yref.shape[0], yref.shape[1], href.size)
# 每個(gè)特征層特定點(diǎn),特定框的label
feat_labels = tf.zeros(shape, dtype=tf.int64) # (m, m, k)
# 每個(gè)特征層特定點(diǎn),特定框的得分
feat_scores = tf.zeros(shape, dtype=dtype)
# 每個(gè)特征層特定點(diǎn),特定框的位置
feat_ymin = tf.zeros(shape, dtype=dtype)
feat_xmin = tf.zeros(shape, dtype=dtype)
feat_ymax = tf.ones(shape, dtype=dtype)
feat_xmax = tf.ones(shape, dtype=dtype)
2、對(duì)所有的實(shí)際框都尋找其在特征層中對(duì)應(yīng)的點(diǎn)與其對(duì)應(yīng)的框,并將其標(biāo)簽找到。
# 用于計(jì)算IOU
def jaccard_with_anchors(bbox):
int_ymin = tf.maximum(ymin, bbox[0]) # (m, m, k)
int_xmin = tf.maximum(xmin, bbox[1])
int_ymax = tf.minimum(ymax, bbox[2])
int_xmax = tf.minimum(xmax, bbox[3])
h = tf.maximum(int_ymax - int_ymin, 0.)
w = tf.maximum(int_xmax - int_xmin, 0.)
# Volumes.
# 處理搜索框和bbox之間的聯(lián)系
inter_vol = h * w # 交集面積
union_vol = vol_anchors - inter_vol \
+ (bbox[2] - bbox[0]) * (bbox[3] - bbox[1]) # 并集面積
jaccard = tf.div(inter_vol, union_vol) # 交集/并集,即IOU
return jaccard # (m, m, k)
def condition(i,feat_labels, feat_scores,
feat_ymin, feat_xmin, feat_ymax, feat_xmax):
r = tf.less(i, tf.shape(labels))
return r[0]
# 該部分用于尋找實(shí)際中的框?qū)?yīng)特征層的哪個(gè)框
def body(i, feat_labels, feat_scores,
feat_ymin, feat_xmin, feat_ymax, feat_xmax):
"""
更新功能標(biāo)簽、分?jǐn)?shù)和bbox。
-JacCard>0.5時(shí)賦值;
"""
# 取出第i個(gè)標(biāo)簽和第i個(gè)bboxes
label = labels[i] # 當(dāng)前圖片上第i個(gè)對(duì)象的標(biāo)簽
bbox = bboxes[i] # 當(dāng)前圖片上第i個(gè)對(duì)象的真實(shí)框bbox
# 計(jì)算該box和所有anchor_box的IOU
jaccard = jaccard_with_anchors(bbox) # 當(dāng)前對(duì)象的bbox和當(dāng)前層的搜索網(wǎng)格IOU
# 所有高于歷史的分的box被篩選
mask = tf.greater(jaccard, feat_scores) # 掩碼矩陣,IOU大于歷史得分的為T(mén)rue
mask = tf.logical_and(mask, feat_scores > -0.5)
imask = tf.cast(mask, tf.int64) #[1,0,1,1,0]
fmask = tf.cast(mask, dtype) #[1.,0.,1.,0. ... ]
# Update values using mask.
# 保證feat_labels存儲(chǔ)對(duì)應(yīng)位置得分最大對(duì)象標(biāo)簽,feat_scores存儲(chǔ)那個(gè)得分
# (m, m, k) × 當(dāng)前類(lèi)別 + (1 - (m, m, k)) × (m, m, k)
# 更新label記錄,此時(shí)的imask已經(jīng)保證了True位置當(dāng)前對(duì)像得分高于之前的對(duì)象得分,其他位置值不變
# 將所有被認(rèn)為是label的框的值賦予feat_labels
feat_labels = imask * label + (1 - imask) * feat_labels
# 用于尋找最匹配的框
feat_scores = tf.where(mask, jaccard, feat_scores)
# 下面四個(gè)矩陣存儲(chǔ)對(duì)應(yīng)label的真實(shí)框坐標(biāo)
# (m, m, k) × 當(dāng)前框坐標(biāo)scalar + (1 - (m, m, k)) × (m, m, k)
feat_ymin = fmask * bbox[0] + (1 - fmask) * feat_ymin
feat_xmin = fmask * bbox[1] + (1 - fmask) * feat_xmin
feat_ymax = fmask * bbox[2] + (1 - fmask) * feat_ymax
feat_xmax = fmask * bbox[3] + (1 - fmask) * feat_xmax
return [i + 1, feat_labels, feat_scores,
feat_ymin, feat_xmin, feat_ymax, feat_xmax]
i = 0
# 2、對(duì)所有的實(shí)際框都尋找其在特征層中對(duì)應(yīng)的點(diǎn)與其對(duì)應(yīng)的框,并將其標(biāo)簽找到。
(i,feat_labels, feat_scores,feat_ymin, feat_xmin,
feat_ymax, feat_xmax) = tf.while_loop(condition, body,
[i,
feat_labels, feat_scores,
feat_ymin, feat_xmin,
feat_ymax, feat_xmax])
3、轉(zhuǎn)化成ssd中網(wǎng)絡(luò)的輸出格式。
# Transform to center / size.
# 3、轉(zhuǎn)化成ssd中網(wǎng)絡(luò)的輸出格式。
feat_cy = (feat_ymax + feat_ymin) / 2.
feat_cx = (feat_xmax + feat_xmin) / 2.
feat_h = feat_ymax - feat_ymin
feat_w = feat_xmax - feat_xmin
# Encode features.
# 利用公式進(jìn)行計(jì)算
# 以搜索網(wǎng)格中心點(diǎn)為參考,真實(shí)框中心的偏移,單位長(zhǎng)度為網(wǎng)格hw
feat_cy = (feat_cy - yref) / href / prior_scaling[0]
feat_cx = (feat_cx - xref) / wref / prior_scaling[1]
# log((m, m, k) / (m, m, 1)) * 5
# 真實(shí)框?qū)捀?搜索網(wǎng)格寬高,取對(duì)
feat_h = tf.log(feat_h / href) / prior_scaling[2]
feat_w = tf.log(feat_w / wref) / prior_scaling[3]
# Use SSD ordering: x / y / w / h instead of ours.(m, m, k, 4)
feat_localizations = tf.stack([feat_cx, feat_cy, feat_w, feat_h], axis=-1)
return feat_labels, feat_localizations, feat_scores
真實(shí)情況下的標(biāo)簽和框在編碼完成后,格式與經(jīng)過(guò)網(wǎng)絡(luò)預(yù)測(cè)出的標(biāo)簽與框相同,此時(shí)才可以計(jì)算loss進(jìn)行對(duì)比。
6、計(jì)算loss值
通過(guò)第五步獲得的框的編碼后的scores和locations指的是數(shù)據(jù)集標(biāo)注的結(jié)果,是真實(shí)情況。而計(jì)算loss值還需要預(yù)測(cè)情況。
通過(guò)如下代碼可以獲得每個(gè)image的預(yù)測(cè)情況,將圖片通過(guò)網(wǎng)絡(luò)進(jìn)行預(yù)測(cè):
# 設(shè)置SSD網(wǎng)絡(luò)的參數(shù)
arg_scope = ssd_net.arg_scope(weight_decay=FLAGS.weight_decay,
data_format=DATA_FORMAT)
# 將圖片經(jīng)過(guò)網(wǎng)絡(luò)獲得它們的框的位置和prediction
with slim.arg_scope(arg_scope):
_, localisations, logits, _ = \
ssd_net.net(b_image, is_training=True)
再調(diào)用loss計(jì)算函數(shù)計(jì)算三個(gè)loss值,分別對(duì)應(yīng)正樣本,負(fù)樣本,定位。
# 計(jì)算loss值
n_positives_loss,n_negative_loss,localization_loss = ssd_net.losses(logits, localisations,
b_gclasses, b_glocalisations, b_gscores,
match_threshold=FLAGS.match_threshold,
negative_ratio=FLAGS.negative_ratio,
alpha=FLAGS.loss_alpha,
label_smoothing=FLAGS.label_smoothing)
# 會(huì)得到三個(gè)loss值,分別對(duì)應(yīng)正樣本,負(fù)樣本,定位
loss_all = n_positives_loss + n_negative_loss + localization_loss
ssd_net.losses中,具體通過(guò)如下方式進(jìn)行損失值的計(jì)算。
1、對(duì)所有的圖片進(jìn)行鋪平,將其種類(lèi)預(yù)測(cè)的轉(zhuǎn)化為(?,num_classes),框預(yù)測(cè)的格式轉(zhuǎn)化為(?,4),實(shí)際種類(lèi)和實(shí)際得分的格式轉(zhuǎn)化為(?),該步可以便于后面的比較與處理。最后將batch個(gè)圖片平鋪到同一表上。
2、在gscores中得到滿足正樣本得分的pmask正樣本,不滿足正樣本得分的為nmask負(fù)樣本,因?yàn)槭褂玫氖莋scores,我們可以知道正樣本負(fù)樣本分類(lèi)是針對(duì)真實(shí)值的。
3、將不滿足正樣本的位置設(shè)成對(duì)應(yīng)prediction中背景的得分,其它設(shè)為1。
4、找到n_neg個(gè)最不可能為背景的點(diǎn)(實(shí)際上它是背景,這樣利用二者計(jì)算的loss就很大)
5、分別計(jì)算正樣本、負(fù)樣本、框的位置的交叉熵。
def ssd_losses(logits, localisations,
gclasses, glocalisations, gscores,
match_threshold=0.5,
negative_ratio=3.,
alpha=1.,
label_smoothing=0.,
device='/cpu:0',
scope=None):
with tf.name_scope(scope, 'ssd_losses'):
lshape = tfe.get_shape(logits[0], 5)
num_classes = lshape[-1]
batch_size = lshape[0]
# 鋪平所有vector
flogits = []
fgclasses = []
fgscores = []
flocalisations = []
fglocalisations = []
for i in range(len(logits)): # 按照?qǐng)D片循環(huán)
flogits.append(tf.reshape(logits[i], [-1, num_classes]))
fgclasses.append(tf.reshape(gclasses[i], [-1]))
fgscores.append(tf.reshape(gscores[i], [-1]))
flocalisations.append(tf.reshape(localisations[i], [-1, 4]))
fglocalisations.append(tf.reshape(glocalisations[i], [-1, 4]))
# 上一步所得的還存在batch個(gè)行里面,對(duì)應(yīng)batch個(gè)圖片
# 這一步將batch個(gè)圖片平鋪到同一表上
logits = tf.concat(flogits, axis=0)
gclasses = tf.concat(fgclasses, axis=0)
gscores = tf.concat(fgscores, axis=0)
localisations = tf.concat(flocalisations, axis=0)
glocalisations = tf.concat(fglocalisations, axis=0)
dtype = logits.dtype
# gscores中滿足正樣本得分的mask
pmask = gscores > match_threshold
fpmask = tf.cast(pmask, dtype)
no_classes = tf.cast(pmask, tf.int32)
nmask = tf.logical_and(tf.logical_not(pmask),# IOU達(dá)不到閾值的類(lèi)別搜索框位置記1
gscores > -0.5)
fnmask = tf.cast(nmask, dtype)
n_positives = tf.reduce_sum(fpmask)
# 將預(yù)測(cè)結(jié)果轉(zhuǎn)化成比率
predictions = slim.softmax(logits)
nvalues = tf.where(nmask,
predictions[:, 0], # 框內(nèi)無(wú)物體標(biāo)記為背景預(yù)測(cè)概率
1. - fnmask) # 框內(nèi)有物體位置標(biāo)記為1
nvalues_flat = tf.reshape(nvalues, [-1])
# max_neg_entries為實(shí)際上負(fù)樣本的個(gè)數(shù)
max_neg_entries = tf.cast(tf.reduce_sum(fnmask), tf.int32)
# n_neg為正樣本的個(gè)數(shù)*3 + batch_size , 之所以+batchsize是因?yàn)槊總€(gè)圖最少有一個(gè)負(fù)樣本背景
n_neg = tf.cast(negative_ratio * n_positives, tf.int32) + batch_size
n_neg = tf.minimum(n_neg, max_neg_entries)
# 找到n_neg個(gè)最不可能為背景的點(diǎn)
val, idxes = tf.nn.top_k(-nvalues_flat, k=n_neg)
max_hard_pred = -val[-1]
# 在nmask找到n_neg個(gè)最不可能為背景的點(diǎn)(實(shí)際上它是背景,這樣二者的差就很大)
nmask = tf.logical_and(nmask, nvalues < max_hard_pred)
fnmask = tf.cast(nmask, dtype)
n_negative = tf.reduce_sum(fnmask)
# 交叉熵
with tf.name_scope('cross_entropy_pos'):
loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits,
labels=gclasses)
n_positives_loss = tf.div(tf.reduce_sum(loss * fpmask), n_positives + 0.1, name='value')
with tf.name_scope('cross_entropy_neg'):
loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits,
labels=no_classes)
n_negative_loss = tf.div(tf.reduce_sum(loss * fnmask), n_negative + 0.1, name='value')
# Add localization loss: smooth L1, L2, ...
with tf.name_scope('localization'):
# Weights Tensor: positive mask + random negative.
weights = tf.expand_dims(alpha * fpmask, axis=-1)
loss = custom_layers.abs_smooth(localisations - glocalisations)
localization_loss = tf.div(tf.reduce_sum(loss * weights), n_positives + 0.1, name='value')
return n_positives_loss,n_negative_loss,localization_loss
7、訓(xùn)練模型并保存
################################優(yōu)化器設(shè)置##############################
learning_rate = tf_utils.configure_learning_rate(FLAGS,
dataset.num_samples,
global_step)
optimizer = tf_utils.configure_optimizer(FLAGS, learning_rate)
train_op = slim.learning.create_train_op(loss_all, optimizer,
summarize_gradients=True)
#################################訓(xùn)練并保存模型###########################
gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=FLAGS.gpu_memory_fraction)
config = tf.ConfigProto(log_device_placement=False,
gpu_options=gpu_options)
saver = tf.train.Saver(max_to_keep=5,
keep_checkpoint_every_n_hours=1.0,
write_version=2,
pad_step_number=False)
slim.learning.train(
train_op, # 優(yōu)化器
logdir=FLAGS.train_dir, # 保存模型的地址
master='',
is_chief=True,
init_fn=tf_utils.get_init_fn(FLAGS), # 微調(diào)已存在模型時(shí),初始化參數(shù)
number_of_steps=FLAGS.max_number_of_steps, # 最大步數(shù)
log_every_n_steps=FLAGS.log_every_n_steps, # 多少時(shí)間進(jìn)行一次命令行輸出
save_summaries_secs=FLAGS.save_summaries_secs, # 進(jìn)行一次summary
saver=saver,
save_interval_secs=FLAGS.save_interval_secs, # 多長(zhǎng)時(shí)間保存一次模型
session_config=config,
sync_optimizer=None)
開(kāi)始訓(xùn)練
在根目錄下創(chuàng)建一個(gè)名為train.sh的文件。利用git上的bash執(zhí)行命令行。

首先轉(zhuǎn)到文件夾中。
cd D:/Collection/SSD-Retry
再執(zhí)行train.sh文件。
bash train.sh
train.sh的代碼如下:
DATASET_DIR=./tfrecords
TRAIN_DIR=./logs/
CHECKPOINT_PATH=./checkpoints/ssd_300_vgg.ckpt
python train_demo.py \
--train_dir=${TRAIN_DIR} \
--dataset_dir=${DATASET_DIR} \
--dataset_name=pascalvoc_2012 \
--dataset_split_name=train \
--model_name=ssd_300_vgg \
--checkpoint_path=${CHECKPOINT_PATH} \
--save_summaries_secs=60 \
--save_interval_secs=600 \
--weight_decay=0.0005 \
--optimizer=adam \
--learning_rate=0.001 \
--batch_size=8
訓(xùn)練效果:

以上就是python目標(biāo)檢測(cè)SSD算法訓(xùn)練部分源碼詳解的詳細(xì)內(nèi)容,更多關(guān)于python目標(biāo)檢測(cè)SSD算法訓(xùn)練的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Django中ajax發(fā)送post請(qǐng)求 報(bào)403錯(cuò)誤CSRF驗(yàn)證失敗解決方案
這篇文章主要介紹了Django中ajax發(fā)送post請(qǐng)求 報(bào)403錯(cuò)誤CSRF驗(yàn)證失敗解決方案,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08
opencv 實(shí)現(xiàn)特定顏色線條提取與定位操作
這篇文章主要介紹了opencv 實(shí)現(xiàn)特定顏色線條提取與定位操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-06-06
在Python中處理字符串之ljust()方法的使用簡(jiǎn)介
這篇文章主要介紹了在Python中處理字符串之ljust()方法的使用,是Python學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-05-05
Python+OpenCV實(shí)現(xiàn)在圖像上繪制矩形
這篇文章主要介紹了如何利用Python和OpenCV實(shí)現(xiàn)在圖像上繪制任意大小的矩形,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以參考一下2022-03-03
python掃描proxy并獲取可用代理ip的實(shí)例
下面小編就為大家?guī)?lái)一篇python掃描proxy并獲取可用代理ip的實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-08-08
Python爬蟲(chóng)爬取煎蛋網(wǎng)圖片代碼實(shí)例
這篇文章主要介紹了Python爬蟲(chóng)爬取煎蛋網(wǎng)圖片代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12
python使用pandas讀取json文件并進(jìn)行刷選導(dǎo)出xlsx文件的方法示例
這篇文章主要介紹了python使用pandas讀取json文件并進(jìn)行刷選導(dǎo)出xlsx文件的方法,結(jié)合實(shí)例形式分析了python調(diào)用pandas模塊針對(duì)json數(shù)據(jù)操作的相關(guān)使用技巧,需要的朋友可以參考下2023-06-06

