Cleaned up ssd module and removed no longer needed code

Signed-off-by: Jim Martens <github@2martens.de>
This commit is contained in:
2019-07-11 11:24:06 +02:00
parent 253a507c90
commit a848c12d64
2 changed files with 31 additions and 502 deletions

View File

@ -228,7 +228,7 @@ def _ssd_train(args: argparse.Namespace) -> None:
history = _ssd_train_call( history = _ssd_train_call(
args, args,
ssd.train_keras, ssd.train,
train_generator, train_generator,
nr_batches_train, nr_batches_train,
val_generator, val_generator,
@ -543,14 +543,14 @@ def _ssd_test(args: argparse.Namespace) -> None:
nr_digits = math.ceil(math.log10(math.ceil(length_dataset / batch_size))) nr_digits = math.ceil(math.log10(math.ceil(length_dataset / batch_size)))
steps_per_epoch = int(math.ceil(length_dataset / batch_size)) steps_per_epoch = int(math.ceil(length_dataset / batch_size))
ssd.predict_keras(test_generator, ssd.predict(test_generator,
steps_per_epoch, steps_per_epoch,
ssd_model, ssd_model,
use_dropout, use_dropout,
forward_passes_per_image, forward_passes_per_image,
(image_size, image_size), (image_size, image_size),
output_path, output_path,
nr_digits) nr_digits)
def _auto_encoder_test(args: argparse.Namespace) -> None: def _auto_encoder_test(args: argparse.Namespace) -> None:

View File

@ -18,138 +18,31 @@
Provides functionality to use the SSD Keras implementation. Provides functionality to use the SSD Keras implementation.
Attributes: Attributes:
IMAGE_SIZE: tuple of (height, width, channels)
N_CLASSES: number of known classes (without background) N_CLASSES: number of known classes (without background)
DROPOUT_RATE: rate for dropping weights
IOU_THRESHOLD: threshold for required overlap with ground truth bounding box
TOP_K: maximum number of predictions kept for each batch item after non-maximum suppression
LOG_FREQUENCY: number of steps that muss pass before logging happens
Classes:
``DropoutSSD``: wraps Dropout SSD 300 model
``SSD``: wraps vanilla SSD 300 model
Functions: Functions:
get_model(...): returns correct SSD model and corresponding predictor sizes
predict(...): runs trained SSD/DropoutSSD on a given data set predict(...): runs trained SSD/DropoutSSD on a given data set
train(...): trains the SSD/DropoutSSD on a given data set train(...): trains the SSD/DropoutSSD on a given data set
""" """
import math
import os import os
import pickle import pickle
import time
from typing import Dict, List, Sequence, Union, Tuple from typing import List, Sequence, Tuple
from typing import Optional from typing import Optional
import numpy as np import numpy as np
import tensorflow as tf import tensorflow as tf
from tensorflow.python.ops import summary_ops_v2
from twomartens.masterthesis.ssd_keras.bounding_box_utils import bounding_box_utils from twomartens.masterthesis.ssd_keras.bounding_box_utils import bounding_box_utils
from twomartens.masterthesis.ssd_keras.data_generator import object_detection_2d_misc_utils from twomartens.masterthesis.ssd_keras.data_generator import object_detection_2d_misc_utils
from twomartens.masterthesis.ssd_keras.keras_loss_function import keras_ssd_loss from twomartens.masterthesis.ssd_keras.keras_loss_function import keras_ssd_loss
from twomartens.masterthesis.ssd_keras.models import keras_ssd300
from twomartens.masterthesis.ssd_keras.models import keras_ssd300_dropout
from twomartens.masterthesis.ssd_keras.ssd_encoder_decoder import ssd_output_decoder from twomartens.masterthesis.ssd_keras.ssd_encoder_decoder import ssd_output_decoder
from twomartens.masterthesis.ssd_keras.ssd_encoder_decoder import ssd_input_encoder
K = tf.keras.backend K = tf.keras.backend
tfe = tf.contrib.eager tfe = tf.contrib.eager
IMAGE_SIZE = (300, 300, 3)
N_CLASSES = 80 N_CLASSES = 80
DROPOUT_RATE = 0.5
IOU_THRESHOLD = 0.45
TOP_K = 200
LOG_FREQUENCY = 10
class SSD:
"""
Wraps vanilla SSD 300 model.
Args:
mode: one of training, inference, and inference_fast
weights_path: path to trained weights
Attributes:
mode: one of training, inference, and inference_fast
predictor_sizes: sizes of predictor layers
model: Keras SSD model
"""
def __init__(self, mode: str, weights_path: Optional[str] = None) -> None:
self.model, self.predictor_sizes = \
keras_ssd300.ssd_300(image_size=IMAGE_SIZE, n_classes=N_CLASSES,
mode=mode, iou_threshold=IOU_THRESHOLD, top_k=TOP_K,
scales=[0.07, 0.15, 0.33, 0.51, 0.69, 0.87, 1.05],
return_predictor_sizes=True) # type: tf.keras.models.Model, np.ndarray
self.mode = mode # type: str
# load existing weights
if weights_path is not None:
self.model.load_weights(weights_path, by_name=True)
if mode == "training":
# set non-classifier layers to non-trainable
classifier_names = ['conv4_3_norm_mbox_conf',
'fc7_mbox_conf',
'conv6_2_mbox_conf',
'conv7_2_mbox_conf',
'conv8_2_mbox_conf',
'conv9_2_mbox_conf']
for layer in self.model.layers:
if layer.name not in classifier_names:
layer.trainable = False
def __call__(self, inputs: tf.Tensor, *args, **kwargs) -> tf.Tensor:
return self.model(inputs)
class DropoutSSD:
"""
Wraps Dropout SSD 300 model.
Args:
mode: one of training, inference, and inference_fast
weights_path: path to trained weights
Attributes:
mode: one of training, inference, and inference_fast
predictor_sizes: sizes of predictor layers
model: Keras SSD model
"""
def __init__(self, mode: str, weights_path: Optional[str] = None) -> None:
self.model, self.predictor_sizes = \
keras_ssd300_dropout.ssd_300_dropout(image_size=IMAGE_SIZE,
n_classes=N_CLASSES,
dropout_rate=DROPOUT_RATE, mode=mode,
iou_threshold=IOU_THRESHOLD,
top_k=TOP_K,
scales=[0.07, 0.15, 0.33, 0.51, 0.69, 0.87, 1.05],
return_predictor_sizes=True) # type: tf.keras.models.Model, np.ndarray
self.mode = mode # type: str
# load existing weights
if weights_path is not None:
self.model.load_weights(weights_path, by_name=True)
if mode == "training":
# set non-classifier layers to non-trainable
classifier_names = ['conv4_3_norm_mbox_conf',
'fc7_mbox_conf',
'conv6_2_mbox_conf',
'conv7_2_mbox_conf',
'conv8_2_mbox_conf',
'conv9_2_mbox_conf']
for layer in self.model.layers:
if layer.name not in classifier_names:
layer.trainable = False
def __call__(self, inputs: tf.Tensor, *args, **kwargs) -> tf.Tensor:
return self.model(inputs)
def get_model(use_dropout: bool, def get_model(use_dropout: bool,
@ -220,14 +113,14 @@ def get_model(use_dropout: bool,
return model, predictor_sizes return model, predictor_sizes
def predict_keras(generator: callable, def predict(generator: callable,
steps_per_epoch: int, steps_per_epoch: int,
ssd_model: tf.keras.models.Model, ssd_model: tf.keras.models.Model,
use_dropout: bool, use_dropout: bool,
forward_passes_per_image: int, forward_passes_per_image: int,
image_size: Tuple[int, int], image_size: Tuple[int, int],
output_path: str, output_path: str,
nr_digits: int) -> None: nr_digits: int) -> None:
""" """
Run trained SSD on the given data set. Run trained SSD on the given data set.
@ -297,168 +190,6 @@ def predict_keras(generator: callable,
break break
def predict(dataset: tf.data.Dataset,
use_dropout: bool,
output_path: str,
weights_path: Optional[str] = None,
checkpoint_path: Optional[str] = None,
verbose: Optional[bool] = False,
forward_passes_per_image: Optional[int] = 42,
nr_digits: Optional[int] = None) -> None:
"""
Run trained SSD on the given data set.
Either the weights path or the checkpoint path must be given. This prevents
a scenario where an untrained network is used to predict.
The prediction results are saved to the output path.
Args:
dataset: the testing data set
use_dropout: if True, DropoutSSD will be used
output_path: the path in which the results should be saved
weights_path: the path to the trained Keras weights (h5 file)
checkpoint_path: the path to the stored checkpoints (Tensorflow checkpoints)
verbose: if True, progress is printed to the standard output
forward_passes_per_image: specifies number of forward passes per image
used by DropoutSSD
nr_digits: number of digits needed to print largest batch number
"""
if weights_path is None and checkpoint_path is None:
raise ValueError("Either 'weights_path' or 'checkpoint_path' must be given.")
# model
if use_dropout:
ssd = DropoutSSD(mode='training', weights_path=weights_path)
else:
ssd = SSD(mode='inference_fast', weights_path=weights_path)
checkpointables = {
'ssd': ssd.model,
'learning_rate_var': K.variable(0),
}
checkpointables.update({
# optimizer
'ssd_optimizer': tf.train.AdamOptimizer(learning_rate=checkpointables['learning_rate_var'],
beta1=0.5, beta2=0.999),
# global step counter
'global_step': tf.train.get_or_create_global_step(),
'epoch_var': K.variable(-1, dtype=tf.int64)
})
if checkpoint_path is not None:
# checkpoint
latest_checkpoint = tf.train.latest_checkpoint(checkpoint_path)
checkpoint = tf.train.Checkpoint(**checkpointables)
checkpoint.restore(latest_checkpoint)
outputs = _predict_one_epoch(dataset, use_dropout, output_path, forward_passes_per_image,
nr_digits, checkpointables['ssd'])
if verbose:
print((
f"predict time: {outputs['per_epoch_time']:.2f}, "
))
print("Prediction finished!... save outputs")
def _predict_one_epoch(dataset: tf.data.Dataset,
use_dropout: bool,
output_path: str,
forward_passes_per_image: int,
nr_digits: int,
ssd: tf.keras.Model) -> Dict[str, float]:
epoch_start_time = time.time()
# prepare filename
filename = 'ssd_predictions'
label_filename = 'ssd_labels'
if use_dropout:
filename = f"dropout-{filename}"
output_file = os.path.join(output_path, filename)
label_output_file = os.path.join(output_path, label_filename)
# go through the data set
counter = 0
for inputs, labels in dataset:
if use_dropout:
detections = None
batch_size = None
for _ in range(forward_passes_per_image):
result = np.array(ssd(inputs))
if batch_size is None:
batch_size = result.shape[0]
if detections is None:
detections = [[] for _ in range(batch_size)]
for i in range(batch_size):
batch_item = result[i]
detections[i].extend(batch_item)
observations = np.asarray(_get_observations(detections))
del detections
observations = ssd_output_decoder.decode_detections_fast(observations,
img_height=IMAGE_SIZE[0],
img_width=IMAGE_SIZE[1])
result_transformed = []
for i in range(batch_size):
# apply inverse transformations to predicted bounding box coordinates
x_reverse = labels[i, 0, 5]
y_reverse = labels[i, 0, 6]
filtered = observations[i]
filtered[:, 2] *= x_reverse
filtered[:, 4] *= x_reverse
filtered[:, 3] *= y_reverse
filtered[:, 5] *= y_reverse
result_transformed.append(filtered)
decoded_predictions_batch = result_transformed
else:
result = np.array(ssd(inputs))
result_filtered = []
# iterate over result of images
for i in range(result.shape[0]):
# apply inverse transformations to predicted bounding box coordinates
# filter out dummy all-zero results
x_reverse = labels[i, 0, 5]
y_reverse = labels[i, 0, 6]
filtered = result[i][result[i, :, 0] != 0]
filtered[:, 2] *= x_reverse
filtered[:, 4] *= x_reverse
filtered[:, 3] *= y_reverse
filtered[:, 5] *= y_reverse
result_filtered.append(filtered)
decoded_predictions_batch = result_filtered
# save predictions batch-wise to prevent memory problems
if nr_digits is not None:
counter_str = str(counter).zfill(nr_digits)
filename = f"{output_file}-{counter_str}.bin"
label_filename = f"{label_output_file}-{counter_str}.bin"
else:
filename = f"{output_file}-{counter:d}.bin"
label_filename = f"{label_output_file}-{counter:d}.bin"
with open(filename, 'wb') as file, open(label_filename, 'wb') as label_file:
pickle.dump(decoded_predictions_batch, file)
pickle.dump(labels, label_file)
counter += 1
epoch_end_time = time.time()
per_epoch_time = epoch_end_time - epoch_start_time
# outputs for epoch
outputs = {
'per_epoch_time': per_epoch_time,
}
return outputs
def _get_observations(detections: Sequence[Sequence[np.ndarray]]) -> List[List[np.ndarray]]: def _get_observations(detections: Sequence[Sequence[np.ndarray]]) -> List[List[np.ndarray]]:
batch_size = len(detections) batch_size = len(detections)
observations = [[] for _ in range(batch_size)] observations = [[] for _ in range(batch_size)]
@ -505,17 +236,17 @@ def _get_observations(detections: Sequence[Sequence[np.ndarray]]) -> List[List[n
return observations return observations
def train_keras(train_generator: callable, def train(train_generator: callable,
steps_per_epoch_train: int, steps_per_epoch_train: int,
val_generator: callable, val_generator: callable,
steps_per_epoch_val: int, steps_per_epoch_val: int,
ssd_model: tf.keras.models.Model, ssd_model: tf.keras.models.Model,
weights_prefix: str, weights_prefix: str,
iteration: int, iteration: int,
initial_epoch: int, initial_epoch: int,
nr_epochs: int, nr_epochs: int,
lr: float, lr: float,
tensorboard_callback: Optional[tf.keras.callbacks.TensorBoard]) -> tf.keras.callbacks.History: tensorboard_callback: Optional[tf.keras.callbacks.TensorBoard]) -> tf.keras.callbacks.History:
""" """
Trains the SSD on the given data set using Keras functionality. Trains the SSD on the given data set using Keras functionality.
@ -576,205 +307,3 @@ def train_keras(train_generator: callable,
ssd_model.save_weights(f"{checkpoint_dir}/ssd300_weights.h5") ssd_model.save_weights(f"{checkpoint_dir}/ssd300_weights.h5")
return history return history
def train(dataset: tf.data.Dataset,
iteration: int,
use_dropout: bool,
length_dataset: int,
weights_prefix: str,
weights_path: Optional[str] = None,
verbose: Optional[bool] = False,
batch_size: Optional[int] = 128,
nr_epochs: Optional[int] = 80,
lr: Optional[float] = 0.002) -> None:
"""
Trains the SSD on the given data set.
This function provides early stopping and creates checkpoints after every
epoch as well as after finishing training. When starting
this function with the same ``iteration`` then the training will try to
continue where it ended last time by restoring a saved checkpoint.
The loss values are provided as scalar summaries.
Args:
dataset: the training data set
iteration: identifier for current training run
use_dropout: if True, the DropoutSSD will be used
length_dataset: specifies number of images in data set
weights_prefix: prefix for weights directory
weights_path: path to the pre-trained SSD weights
verbose: if True, progress is printed to the standard output
batch_size: size of each batch
nr_epochs: number of epochs to train
lr: initial learning rate
"""
# define checkpointed tensors and variables
checkpointables = {
'learning_rate_var': K.variable(lr),
}
# model
if use_dropout:
ssd = DropoutSSD(mode='training', weights_path=weights_path)
else:
ssd = SSD(mode='training', weights_path=weights_path)
checkpointables.update({
'ssd': ssd.model
})
checkpointables.update({
# optimizer
'ssd_optimizer': tf.train.AdamOptimizer(learning_rate=checkpointables['learning_rate_var'],
beta1=0.5, beta2=0.999),
# global step counter
'global_step': tf.train.get_or_create_global_step(),
'epoch_var': K.variable(-1, dtype=tf.int64)
})
# checkpoint
checkpoint_dir = os.path.join(weights_prefix, str(iteration) + '/')
os.makedirs(checkpoint_dir, exist_ok=True)
checkpoint_prefix = os.path.join(checkpoint_dir, 'ckpt')
latest_checkpoint = tf.train.latest_checkpoint(checkpoint_dir)
checkpoint = tf.train.Checkpoint(**checkpointables)
checkpoint.restore(latest_checkpoint)
# update model inside SSD object with version from checkpoint
ssd.model = checkpointables['ssd']
# input encoder
input_encoder = ssd_input_encoder.SSDInputEncoder(IMAGE_SIZE[0], IMAGE_SIZE[1],
N_CLASSES, ssd.predictor_sizes,
steps=[8, 16, 32, 64, 100, 300],
aspect_ratios_per_layer=[[1.0, 2.0, 0.5],
[1.0, 2.0, 0.5, 3.0, 1.0 / 3.0],
[1.0, 2.0, 0.5, 3.0, 1.0 / 3.0],
[1.0, 2.0, 0.5, 3.0, 1.0 / 3.0],
[1.0, 2.0, 0.5],
[1.0, 2.0, 0.5]])
with summary_ops_v2.always_record_summaries():
summary_ops_v2.scalar(name='learning_rate', tensor=checkpointables['learning_rate_var'],
step=checkpointables['global_step'])
nr_batches_per_epoch = int(math.ceil(length_dataset / float(batch_size)))
_train_epochs(nr_batches_per_epoch, nr_epochs, dataset, input_encoder,
checkpoint, checkpoint_prefix, verbose=verbose, **checkpointables)
if verbose:
print("Training finished!... save model weights")
# save trained models
checkpoint.save(checkpoint_prefix)
def _train_epochs(nr_batches_per_epoch: int,
nr_epochs: int,
dataset: tf.data.Dataset,
input_encoder: ssd_input_encoder.SSDInputEncoder,
checkpoint: tf.train.Checkpoint,
checkpoint_prefix: str,
ssd: tf.keras.Model,
ssd_optimizer: tf.train.Optimizer,
global_step: tf.Variable,
epoch_var: tf.Variable,
learning_rate_var: tf.Variable,
verbose: bool) -> None:
with summary_ops_v2.always_record_summaries():
epoch = 0
batch_counter = 0
epoch_var.assign(epoch)
# go through data set
for x, y in dataset:
if batch_counter == 0:
# epoch starts
epoch_start_time = time.time()
ssd_loss_avg = tfe.metrics.Mean(name='ssd_loss', dtype=tf.float32)
if verbose:
print((
f"epoch: {epoch + 1:d}"
))
labels = []
for i in range(y.shape[0]):
image_labels = np.asarray(y[i])
image_labels = image_labels[image_labels[:, 0] != -1]
labels.append(image_labels)
encoded_ground_truth = input_encoder(labels)
ssd_train_loss = _train_ssd_step(ssd=ssd,
optimizer=ssd_optimizer,
inputs=x,
ground_truth=encoded_ground_truth,
global_step=global_step)
ssd_loss_avg(ssd_train_loss)
global_step.assign_add(1)
batch_counter += 1
if batch_counter == nr_batches_per_epoch:
# one epoch is over
epoch_end_time = time.time()
per_epoch_time = epoch_end_time - epoch_start_time
# final losses of epoch
outputs = {
'ssd_loss': ssd_loss_avg.result(False),
'per_epoch_time': per_epoch_time,
}
if verbose:
print((
f"[{epoch + 1:d}/{nr_epochs:d}] - "
f"train time: {outputs['per_epoch_time']:.2f}, "
f"SSD loss: {outputs['ssd_loss']:.3f}, "
f"batch_counter: {batch_counter:d}, "
f"nr_batches_per_epoch: {nr_batches_per_epoch:d}"
))
# save weights at end of epoch
checkpoint.save(checkpoint_prefix)
epoch += 1
batch_counter = 0
def _train_ssd_step(ssd: tf.keras.Model,
optimizer: tf.train.Optimizer,
inputs: tf.Tensor,
ground_truth: tf.Tensor,
global_step: tf.Variable) -> tf.Tensor:
"""
Trains the SSD model for one step (one batch).
:param ssd: instance of the SSD model
:param optimizer: instance of chosen optimizer
:param inputs: inputs from data set
:param ground_truth: ground truth from data set
:param global_step: the global step variable
:return: the calculated loss
"""
with tf.GradientTape() as tape:
predictions = ssd(inputs)
loss = keras_ssd_loss.SSDLoss()
batch_size = tf.shape(predictions)[0]
ssd_loss = loss.compute_loss(ground_truth, predictions) / tf.to_float(batch_size)
ssd_grads = tape.gradient(ssd_loss, ssd.trainable_variables)
if int(global_step % LOG_FREQUENCY) == 0:
summary_ops_v2.scalar(name='ssd_loss', tensor=ssd_loss, step=global_step)
for grad, variable in zip(ssd_grads, ssd.trainable_variables):
summary_ops_v2.histogram(name='gradients/' + variable.name, tensor=tf.math.l2_normalize(grad),
step=global_step)
summary_ops_v2.histogram(name='variables/' + variable.name, tensor=tf.math.l2_normalize(variable),
step=global_step)
optimizer.apply_gradients(zip(ssd_grads, ssd.trainable_variables),
global_step=global_step)
return ssd_loss