python目標(biāo)檢測SSD算法訓(xùn)練部分源碼詳解
學(xué)習(xí)前言
……又看了很久的SSD算法,今天講解一下訓(xùn)練部分的代碼。
預(yù)測部分的代碼可以參照http://www.dbjr.com.cn/article/246905.htm
講解構(gòu)架
本次教程的講解主要是對(duì)訓(xùn)練部分的代碼進(jìn)行講解,該部分講解主要是對(duì)訓(xùn)練函數(shù)的執(zhí)行過程與執(zhí)行思路進(jìn)行詳解。
訓(xùn)練函數(shù)的執(zhí)行過程大體上分為:
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ù)測結(jié)果,便于比較。
6、計(jì)算loss值。
7、利用優(yōu)化器完成梯度下降并保存模型。
在看本次算法前,建議先下載我簡化過的源碼,配合觀看,具體運(yùn)行方法在開始訓(xùn)練部分
下載鏈接 https://pan.baidu.com/s/1K4RAJvLj11blywuX2CrLSA
提取碼:4wbi
模型訓(xùn)練的流程
本文使用的ssd_vgg_300的源碼點(diǎn)擊下載,本文對(duì)其進(jìn)行了簡化,保留了上一次篩選出的預(yù)測部分,還加入了訓(xùn)練部分,便于理順整個(gè)SSD的框架。
1、設(shè)置參數(shù)
在載入數(shù)據(jù)庫前,首先要設(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ù)集種類個(gè)數(shù) tf.app.flags.DEFINE_integer( 'num_classes', 21, 'Number of classes to use in the dataset.') # 訓(xùn)練還是測試 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ù)看起來多,其實(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)練流程中,其通過如下函數(shù)讀取數(shù)據(jù)集
##########################讀取數(shù)據(jù)集部分############################# # 選擇數(shù)據(jù)庫 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)練還是測試 dataset_dir: 存儲(chǔ)數(shù)據(jù)集文件的目錄。 file_pattern: 用于匹配數(shù)據(jù)集源文件的文件模式。 reader: tf.readerbase的子類。如果保留為“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í)行過程如下,其中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)練還是測試 dataset_dir: 數(shù)據(jù)集的位置 file_pattern: 匹配數(shù)據(jù)集源時(shí)要使用的文件模式。 假定模式包含一個(gè)'%s'字符串,以便可以插入拆分名稱 reader: TensorFlow閱讀器類型。 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í)際上就開始對(duì)tfrecord的文件進(jìn)行分割了,通過代碼注釋我們了解代碼的執(zhí)行過程,其中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)練還是測試 dataset_dir: 存儲(chǔ)數(shù)據(jù)集文件的目錄。 file_pattern: 用于匹配數(shù)據(jù)集源文件的文件模式。 reader: tf.readerbase的子類。如果保留為“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)沒有的時(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, # 種類 labels_to_names=labels_to_names)
通過上述一系列操作,實(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ò)的過程并不復(fù)雜,沒有許多函數(shù)的調(diào)用,實(shí)際執(zhí)行過程如果了解ssd網(wǎng)絡(luò)的預(yù)測部分就很好理解,我這里只講下邏輯:
1、利用ssd_class = ssd_vgg_300.SSDNet獲得SSDNet的類
2、替換種類的數(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 # 替換種類的數(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ù)集的代碼比較長,但是邏輯并不難理解。
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ù). 通過配置, # 可以同時(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í)際上最終是通過preprocess_for_train處理數(shù)據(jù)集。
preprocess_for_train處理的過程是:
1、改變數(shù)據(jù)類型。
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: 處理過的圖片 """ 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ù)類型 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]) # 圖像的類型 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大于歷史得分的為True 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)前類別 + (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í)框中心的偏移,單位長度為網(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)過網(wǎng)絡(luò)預(yù)測出的標(biāo)簽與框相同,此時(shí)才可以計(jì)算loss進(jìn)行對(duì)比。
6、計(jì)算loss值
通過第五步獲得的框的編碼后的scores和locations指的是數(shù)據(jù)集標(biāo)注的結(jié)果,是真實(shí)情況。而計(jì)算loss值還需要預(yù)測情況。
通過如下代碼可以獲得每個(gè)image的預(yù)測情況,將圖片通過網(wǎng)絡(luò)進(jìn)行預(yù)測:
# 設(shè)置SSD網(wǎng)絡(luò)的參數(shù) arg_scope = ssd_net.arg_scope(weight_decay=FLAGS.weight_decay, data_format=DATA_FORMAT) # 將圖片經(jīng)過網(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中,具體通過如下方式進(jìn)行損失值的計(jì)算。
1、對(duì)所有的圖片進(jìn)行鋪平,將其種類預(yù)測的轉(zhuǎn)化為(?,num_classes),框預(yù)測的格式轉(zhuǎn)化為(?,4),實(shí)際種類和實(shí)際得分的格式轉(zhuǎn)化為(?),該步可以便于后面的比較與處理。最后將batch個(gè)圖片平鋪到同一表上。
2、在gscores中得到滿足正樣本得分的pmask正樣本,不滿足正樣本得分的為nmask負(fù)樣本,因?yàn)槭褂玫氖莋scores,我們可以知道正樣本負(fù)樣本分類是針對(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á)不到閾值的類別搜索框位置記1 gscores > -0.5) fnmask = tf.cast(nmask, dtype) n_positives = tf.reduce_sum(fpmask) # 將預(yù)測結(jié)果轉(zhuǎn)化成比率 predictions = slim.softmax(logits) nvalues = tf.where(nmask, predictions[:, 0], # 框內(nèi)無物體標(biāo)記為背景預(yù)測概率 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, # 多長時(shí)間保存一次模型 session_config=config, sync_optimizer=None)
開始訓(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)檢測SSD算法訓(xùn)練部分源碼詳解的詳細(xì)內(nèi)容,更多關(guān)于python目標(biāo)檢測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)證失敗解決方案,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08opencv 實(shí)現(xiàn)特定顏色線條提取與定位操作
這篇文章主要介紹了opencv 實(shí)現(xiàn)特定顏色線條提取與定位操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-06-06Python+OpenCV實(shí)現(xiàn)在圖像上繪制矩形
這篇文章主要介紹了如何利用Python和OpenCV實(shí)現(xiàn)在圖像上繪制任意大小的矩形,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以參考一下2022-03-03python掃描proxy并獲取可用代理ip的實(shí)例
下面小編就為大家?guī)硪黄猵ython掃描proxy并獲取可用代理ip的實(shí)例。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-08-08Python爬蟲爬取煎蛋網(wǎng)圖片代碼實(shí)例
這篇文章主要介紹了Python爬蟲爬取煎蛋網(wǎng)圖片代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12python使用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