diff --git a/.gitignore b/.gitignore index 21e7c55cb619380276529b7c3ce9099559779151..9175441846bc40d093c736ca29172230935f556e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ sphinx dist record.txt build/ +*.DS_Store diff --git a/bob/bio/face/tensorflow/__init__.py b/bob/bio/face/tensorflow/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/bob/bio/face/tensorflow/preprocessing.py b/bob/bio/face/tensorflow/preprocessing.py new file mode 100644 index 0000000000000000000000000000000000000000..bf87921c171140bb2f364bae7715da744fc4c8b5 --- /dev/null +++ b/bob/bio/face/tensorflow/preprocessing.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +Tensor pre-processing for somr face recognition CNNs +""" + +import os +from functools import partial +from multiprocessing import cpu_count +import tensorflow as tf +from tensorflow.keras import layers + +# STANDARD FEATURES FROM OUR TF-RECORDS +FEATURES = { + "data": tf.io.FixedLenFeature([], tf.string), + "label": tf.io.FixedLenFeature([], tf.int64), + "key": tf.io.FixedLenFeature([], tf.string), +} + + +def decode_tfrecords(x, data_shape, data_type=tf.uint8): + features = tf.io.parse_single_example(x, FEATURES) + image = tf.io.decode_raw(features["data"], data_type) + image = tf.reshape(image, data_shape) + features["data"] = image + return features + + +def get_preprocessor(output_shape): + """ + + """ + preprocessor = tf.keras.Sequential( + [ + # rotate before cropping + # 5 random degree rotation + layers.experimental.preprocessing.RandomRotation(5 / 360), + layers.experimental.preprocessing.RandomCrop( + height=output_shape[0], width=output_shape[1] + ), + layers.experimental.preprocessing.RandomFlip("horizontal"), + # FIXED_STANDARDIZATION from https://github.com/davidsandberg/facenet + # [-0.99609375, 0.99609375] + layers.experimental.preprocessing.Rescaling( + scale=1 / 128, offset=-127.5 / 128 + ), + ] + ) + return preprocessor + + +def preprocess(preprocessor, features, augment=False): + image = features["data"] + label = features["label"] + image = preprocessor(image, training=augment) + return image, label + + +def prepare_dataset( + tf_record_paths, + batch_size, + epochs, + data_shape, + output_shape, + shuffle=False, + augment=False, + autotune=tf.data.experimental.AUTOTUNE, + n_cpus=cpu_count(), + shuffle_buffer=int(2e4), +): + """ + Create batches from a list of TF-Records + + Parameters + ---------- + + tf_record_paths: list + List of paths of the TF-Records + + batch_size: int + + epochs: int + + shuffle: bool + + augment: bool + + autotune: int + + n_cpus: int + + shuffle_buffer: int + """ + + ds = tf.data.Dataset.list_files(tf_record_paths, shuffle=shuffle) + ds = tf.data.TFRecordDataset(ds, num_parallel_reads=n_cpus) + if shuffle: + # ignore order and read files as soon as they come in + ignore_order = tf.data.Options() + ignore_order.experimental_deterministic = False + ds = ds.with_options(ignore_order) + ds = ds.map(partial(decode_tfrecords, data_shape=data_shape)).prefetch(buffer_size=autotune) + if shuffle: + ds = ds.shuffle(shuffle_buffer).repeat(epochs) + preprocessor = get_preprocessor(output_shape) + ds = ds.batch(batch_size).map( + partial(preprocess, preprocessor, augment=augment), num_parallel_calls=autotune, + ) + + # Use buffered prefecting on all datasets + return ds.prefetch(buffer_size=autotune) diff --git a/cnn_training/._centerloss_mixed_precision.py b/cnn_training/._centerloss_mixed_precision.py new file mode 100644 index 0000000000000000000000000000000000000000..881da051d61cfeb4e02036b9ea9c9df85b2fefdf Binary files /dev/null and b/cnn_training/._centerloss_mixed_precision.py differ diff --git a/cnn_training/centerloss.py b/cnn_training/centerloss.py new file mode 100644 index 0000000000000000000000000000000000000000..50511ad764624948cc67987053a9f8f9ab9a5372 --- /dev/null +++ b/cnn_training/centerloss.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +Trains a face recognition CNN using the strategy from the paper + +"A Discriminative Feature Learning Approach +for Deep Face Recognition" https://ydwen.github.io/papers/WenECCV16.pdf + +The default backbone is the InceptionResnetv2 + +Do `./bin/python centerloss.py --help` for more information + +""" + +import os +from functools import partial +import click +import pkg_resources +import tensorflow as tf +from bob.learn.tensorflow.losses import CenterLoss, CenterLossLayer +from bob.learn.tensorflow.models.inception_resnet_v2 import InceptionResNetV2 +from bob.learn.tensorflow.metrics import predict_using_tensors +from tensorflow.keras import layers +from bob.learn.tensorflow.callbacks import add_backup_callback +from bob.learn.tensorflow.metrics.embedding_accuracy import accuracy_from_embeddings +from bob.extension import rc +from bob.bio.face.tensorflow.preprocessing import prepare_dataset + +# CNN Backbone +# Change your NN backbone here +BACKBONE = InceptionResNetV2 + +# SHAPES EXPECTED FROM THE DATASET USING THIS BACKBONE +DATA_SHAPE = (182, 182, 3) # size of faces +DATA_TYPE = tf.uint8 +OUTPUT_SHAPE = (160, 160) + +AUTOTUNE = tf.data.experimental.AUTOTUNE + +# HERE WE VALIDATE WITH LFW RUNNING A +# INFORMATION ABOUT THE VALIDATION SET +VALIDATION_TF_RECORD_PATHS = rc["bob.bio.face.cnn.lfw_tfrecord_path"] + +# there are 2812 samples in the validation set +VALIDATION_SAMPLES = 2812 +VALIDATION_BATCH_SIZE = 38 + +# WEIGHTS BEWTWEEN the two losses +LOSS_WEIGHTS = {"cross_entropy": 1.0, "center_loss": 0.01} + + +class CenterLossModel(tf.keras.Model): + def compile( + self, + cross_entropy, + center_loss, + loss_weights, + train_loss, + train_cross_entropy, + train_center_loss, + test_acc, + global_batch_size, + **kwargs, + ): + super().compile(**kwargs) + self.cross_entropy = cross_entropy + self.center_loss = center_loss + self.loss_weights = loss_weights + self.train_loss = train_loss + self.train_cross_entropy = train_cross_entropy + self.train_center_loss = train_center_loss + self.test_acc = test_acc + self.global_batch_size = global_batch_size + + def train_step(self, data): + images, labels = data + with tf.GradientTape() as tape: + logits, prelogits = self(images, training=True) + loss_cross = self.cross_entropy(labels, logits) + loss_center = self.center_loss(labels, prelogits) + loss = ( + loss_cross * self.loss_weights[self.cross_entropy.name] + + loss_center * self.loss_weights[self.center_loss.name] + ) + trainable_vars = self.trainable_variables + gradients = tape.gradient(loss, trainable_vars) + self.optimizer.apply_gradients(zip(gradients, trainable_vars)) + + self.train_loss(loss) + self.train_cross_entropy(loss_cross) + self.train_center_loss(loss_center) + return { + m.name: m.result() + for m in [self.train_loss, self.train_cross_entropy, self.train_center_loss] + } + + def test_step(self, data): + images, labels = data + logits, prelogits = self(images, training=False) + self.test_acc(accuracy_from_embeddings(labels, prelogits)) + return {m.name: m.result() for m in [self.test_acc]} + + +def create_model(n_classes): + + model = BACKBONE( + include_top=True, + classes=n_classes, + bottleneck=True, + input_shape=OUTPUT_SHAPE + (3,), + ) + + prelogits = model.get_layer("Bottleneck/BatchNorm").output + prelogits = CenterLossLayer( + n_classes=n_classes, n_features=prelogits.shape[-1], name="centers" + )(prelogits) + + logits = model.get_layer("logits").output + model = CenterLossModel( + inputs=model.input, outputs=[logits, prelogits], name=model.name + ) + return model + + +def build_and_compile_model(n_classes, learning_rate, global_batch_size): + model = create_model(n_classes) + + cross_entropy = tf.keras.losses.SparseCategoricalCrossentropy( + from_logits=True, name="cross_entropy", reduction=tf.keras.losses.Reduction.NONE + ) + center_loss = CenterLoss( + centers_layer=model.get_layer("centers"), + alpha=0.9, + name="center_loss", + reduction=tf.keras.losses.Reduction.NONE, + ) + + optimizer = tf.keras.optimizers.RMSprop( + learning_rate=learning_rate, rho=0.9, momentum=0.9, epsilon=1.0 + ) + + train_loss = tf.keras.metrics.Mean(name="loss") + train_cross_entropy = tf.keras.metrics.Mean(name="cross_entropy") + train_center_loss = tf.keras.metrics.Mean(name="center_loss") + + test_acc = tf.keras.metrics.Mean(name="accuracy") + + model.compile( + optimizer=optimizer, + cross_entropy=cross_entropy, + center_loss=center_loss, + loss_weights=LOSS_WEIGHTS, + train_loss=train_loss, + train_cross_entropy=train_cross_entropy, + train_center_loss=train_center_loss, + test_acc=test_acc, + global_batch_size=global_batch_size, + ) + return model + + +@click.command() +@click.argument("tf-record-paths") +@click.argument("checkpoint-path") +@click.option( + "-n", + "--n-classes", + default=87662, + help="Number of classes in the classification problem. Default to `87662`, which is the number of identities in our pruned MSCeleb", +) +@click.option( + "-b", + "--batch-size", + default=90, + help="Batch size. Be aware that we are using single precision. Batch size should be high.", +) +@click.option( + "-e", "--epochs", default=35, help="Number of epochs", +) +def train_and_evaluate(tf_record_paths, checkpoint_path, n_classes, batch_size, epochs): + # number of training steps to do before validating a model. This also defines an epoch + # for keras which is not really true. We want to evaluate every 180000 (90 * 2000) + # samples + STEPS_PER_EPOCH = 180000 // batch_size + learning_rate = 0.1 + KERAS_EPOCH_MULTIPLIER = 6 + train_ds = prepare_dataset( + tf_record_paths, + batch_size, + epochs, + data_shape=DATA_SHAPE, + output_shape=OUTPUT_SHAPE, + shuffle=True, + augment=True + ) + + if VALIDATION_TF_RECORD_PATHS is None: + raise ValueError("No validation set was set. Please, do `bob config set bob.bio.face.cnn.lfw_tfrecord_path [PATH]`") + + val_ds = prepare_dataset( + VALIDATION_TF_RECORD_PATHS, + data_shape=DATA_SHAPE, + output_shape=OUTPUT_SHAPE, + epochs=epochs, + batch_size=VALIDATION_BATCH_SIZE, + shuffle=False, + augment=False, + ) + val_metric_name = "val_accuracy" + + model = build_and_compile_model(n_classes, learning_rate, global_batch_size=batch_size) + + + def scheduler(epoch, lr): + # 20 epochs at 0.1, 10 at 0.01 and 5 0.001 + # The epoch number here is Keras's which is different from actual epoch number + epoch = epoch // KERAS_EPOCH_MULTIPLIER + if epoch in range(20): + return 0.1 + elif epoch in range(20, 30): + return 0.01 + else: + return 0.001 + + callbacks = { + "latest": tf.keras.callbacks.ModelCheckpoint(f"{checkpoint_path}/latest", verbose=1), + "best": tf.keras.callbacks.ModelCheckpoint( + f"{checkpoint_path}/best", + monitor=val_metric_name, + save_best_only=True, + mode="max", + verbose=1, + ), + "tensorboard": tf.keras.callbacks.TensorBoard( + log_dir=f"{checkpoint_path}/logs", update_freq=15, profile_batch="10,50" + ), + "lr": tf.keras.callbacks.LearningRateScheduler(scheduler, verbose=1), + "nan":tf.keras.callbacks.TerminateOnNaN(), + } + callbacks = add_backup_callback(callbacks, backup_dir=f"{checkpoint_path}/backup") + model.fit( + train_ds, + validation_data=val_ds, + epochs=epochs * KERAS_EPOCH_MULTIPLIER, + steps_per_epoch=STEPS_PER_EPOCH, + validation_steps=VALIDATION_SAMPLES // VALIDATION_BATCH_SIZE, + callbacks=callbacks, + verbose=2, + ) + + +if __name__ == "__main__": + train_and_evaluate() diff --git a/cnn_training/centerloss_mixed_precision.py b/cnn_training/centerloss_mixed_precision.py new file mode 100644 index 0000000000000000000000000000000000000000..ecd059f3504606f236bff241304f844db37a630d --- /dev/null +++ b/cnn_training/centerloss_mixed_precision.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +Trains a face recognition CNN using the strategy from the paper + +"A Discriminative Feature Learning Approach +for Deep Face Recognition" https://ydwen.github.io/papers/WenECCV16.pdf + +######### +# THIS ONE USES FLOAT16 TO COMPUTE THE GRADIENTS +# CHECKE HERE FOR MORE INFO: # https://www.tensorflow.org/api_docs/python/tf/keras/mixed_precision/experimental/Policy +######## + +The default backbone is the InceptionResnetv2 + +Do `./bin/python centerloss_mixed_precision.py --help` for more information + +""" + +import os +from functools import partial +import click +import pkg_resources +import tensorflow as tf +from bob.learn.tensorflow.losses import CenterLoss, CenterLossLayer +from bob.learn.tensorflow.models.inception_resnet_v2 import InceptionResNetV2 +from bob.learn.tensorflow.metrics import predict_using_tensors +from tensorflow.keras import layers +from tensorflow.keras.mixed_precision import experimental as mixed_precision +from bob.learn.tensorflow.callbacks import add_backup_callback +from bob.learn.tensorflow.metrics.embedding_accuracy import accuracy_from_embeddings +from bob.extension import rc +from bob.bio.face.tensorflow.preprocessing import prepare_dataset + +# Setting mixed precision policy +# https://www.tensorflow.org/api_docs/python/tf/keras/mixed_precision/experimental/Policy +policy = mixed_precision.Policy("mixed_float16") +mixed_precision.set_policy(policy) + +# CNN Backbone +# Change your NN backbone here +BACKBONE = InceptionResNetV2 + +# SHAPES EXPECTED FROM THE DATASET USING THIS BACKBONE +DATA_SHAPE = (182, 182, 3) # size of faces +DATA_TYPE = tf.uint8 +OUTPUT_SHAPE = (160, 160) + +AUTOTUNE = tf.data.experimental.AUTOTUNE + +# HERE WE VALIDATE WITH LFW RUNNING A +# INFORMATION ABOUT THE VALIDATION SET +VALIDATION_TF_RECORD_PATHS = rc["bob.bio.face.cnn.lfw_tfrecord_path"] + +# there are 2812 samples in the validation set +VALIDATION_SAMPLES = 2812 +VALIDATION_BATCH_SIZE = 38 + +# WEIGHTS BEWTWEEN the two losses +LOSS_WEIGHTS = {"cross_entropy": 1.0, "center_loss": 0.01} + + +class CenterLossModel(tf.keras.Model): + def compile( + self, + cross_entropy, + center_loss, + loss_weights, + train_loss, + train_cross_entropy, + train_center_loss, + test_acc, + global_batch_size, + **kwargs, + ): + super().compile(**kwargs) + self.cross_entropy = cross_entropy + self.center_loss = center_loss + self.loss_weights = loss_weights + self.train_loss = train_loss + self.train_cross_entropy = train_cross_entropy + self.train_center_loss = train_center_loss + self.test_acc = test_acc + self.global_batch_size = global_batch_size + + def train_step(self, data): + images, labels = data + with tf.GradientTape() as tape: + logits, prelogits = self(images, training=True) + loss_cross = self.cross_entropy(labels, logits) + loss_center = self.center_loss(labels, prelogits) + loss = ( + loss_cross * self.loss_weights[self.cross_entropy.name] + + loss_center * self.loss_weights[self.center_loss.name] + ) + unscaled_loss = tf.nn.compute_average_loss( + loss, global_batch_size=self.global_batch_size + ) + loss = self.optimizer.get_scaled_loss(unscaled_loss) + + trainable_vars = self.trainable_variables + gradients = tape.gradient(loss, trainable_vars) + gradients = self.optimizer.get_unscaled_gradients(gradients) + self.optimizer.apply_gradients(zip(gradients, trainable_vars)) + + self.train_loss(unscaled_loss) + self.train_cross_entropy(loss_cross) + self.train_center_loss(loss_center) + return { + m.name: m.result() + for m in [self.train_loss, self.train_cross_entropy, self.train_center_loss] + } + + def test_step(self, data): + images, labels = data + logits, prelogits = self(images, training=False) + self.test_acc(accuracy_from_embeddings(labels, prelogits)) + return {m.name: m.result() for m in [self.test_acc]} + + +def create_model(n_classes): + + model = BACKBONE( + include_top=True, + classes=n_classes, + bottleneck=True, + input_shape=OUTPUT_SHAPE + (3,), + kernel_regularizer=tf.keras.regularizers.L2(5e-5) + ) + float32_layer = layers.Activation("linear", dtype="float32") + + prelogits = model.get_layer("Bottleneck/BatchNorm").output + prelogits = CenterLossLayer( + n_classes=n_classes, n_features=prelogits.shape[-1], name="centers" + )(prelogits) + prelogits = float32_layer(prelogits) + logits = float32_layer(model.get_layer("logits").output) + model = CenterLossModel( + inputs=model.input, outputs=[logits, prelogits], name=model.name + ) + return model + + +def build_and_compile_model(n_classes, learning_rate, global_batch_size): + model = create_model(n_classes) + + cross_entropy = tf.keras.losses.SparseCategoricalCrossentropy( + from_logits=True, name="cross_entropy", reduction=tf.keras.losses.Reduction.NONE + ) + center_loss = CenterLoss( + centers_layer=model.get_layer("centers"), + alpha=0.9, + name="center_loss", + reduction=tf.keras.losses.Reduction.NONE, + ) + + optimizer = tf.keras.optimizers.RMSprop( + learning_rate=learning_rate, rho=0.9, momentum=0.9, epsilon=1.0 + ) + optimizer = mixed_precision.LossScaleOptimizer(optimizer, loss_scale="dynamic") + + train_loss = tf.keras.metrics.Mean(name="loss") + train_cross_entropy = tf.keras.metrics.Mean(name="cross_entropy") + train_center_loss = tf.keras.metrics.Mean(name="center_loss") + + test_acc = tf.keras.metrics.Mean(name="accuracy") + + model.compile( + optimizer=optimizer, + cross_entropy=cross_entropy, + center_loss=center_loss, + loss_weights=LOSS_WEIGHTS, + train_loss=train_loss, + train_cross_entropy=train_cross_entropy, + train_center_loss=train_center_loss, + test_acc=test_acc, + global_batch_size=global_batch_size, + ) + return model + + +@click.command() +@click.argument("tf-record-paths") +@click.argument("checkpoint-path") +@click.option( + "-n", + "--n-classes", + default=87662, + help="Number of classes in the classification problem. Default to `87662`, which is the number of identities in our pruned MSCeleb", +) +@click.option( + "-b", + "--batch-size", + default=90 * 2, + help="Batch size. Be aware that we are using single precision. Batch size should be high.", +) +@click.option( + "-e", "--epochs", default=35, help="Number of epochs", +) +def train_and_evaluate(tf_record_paths, checkpoint_path, n_classes, batch_size, epochs): + # number of training steps to do before validating a model. This also defines an epoch + # for keras which is not really true. We want to evaluate every 180000 (90 * 2000) + # samples + STEPS_PER_EPOCH = 180000 // batch_size + learning_rate = 0.1 + KERAS_EPOCH_MULTIPLIER = 6 + train_ds = prepare_dataset( + tf_record_paths, + batch_size, + epochs, + data_shape=DATA_SHAPE, + output_shape=OUTPUT_SHAPE, + shuffle=True, + augment=True + ) + + if VALIDATION_TF_RECORD_PATHS is None: + raise ValueError("No validation set was set. Please, do `bob config set bob.bio.face.cnn.lfw_tfrecord_path [PATH]`") + + val_ds = prepare_dataset( + VALIDATION_TF_RECORD_PATHS, + data_shape=DATA_SHAPE, + output_shape=OUTPUT_SHAPE, + epochs=epochs, + batch_size=VALIDATION_BATCH_SIZE, + shuffle=False, + augment=False, + ) + val_metric_name = "val_accuracy" + + model = build_and_compile_model(n_classes, learning_rate, global_batch_size=batch_size) + + + def scheduler(epoch, lr): + # 20 epochs at 0.1, 10 at 0.01 and 5 0.001 + # The epoch number here is Keras's which is different from actual epoch number + epoch = epoch // KERAS_EPOCH_MULTIPLIER + if epoch in range(20): + return 0.1 + elif epoch in range(20, 30): + return 0.01 + else: + return 0.001 + + callbacks = { + "latest": tf.keras.callbacks.ModelCheckpoint(f"{checkpoint_path}/latest", verbose=1), + "best": tf.keras.callbacks.ModelCheckpoint( + f"{checkpoint_path}/best", + monitor=val_metric_name, + save_best_only=True, + mode="max", + verbose=1, + ), + "tensorboard": tf.keras.callbacks.TensorBoard( + log_dir=f"{checkpoint_path}/logs", update_freq=15, profile_batch="10,50" + ), + "lr": tf.keras.callbacks.LearningRateScheduler(scheduler, verbose=1), + "nan":tf.keras.callbacks.TerminateOnNaN(), + } + callbacks = add_backup_callback(callbacks, backup_dir=f"{checkpoint_path}/backup") + model.fit( + train_ds, + validation_data=val_ds, + epochs=epochs * KERAS_EPOCH_MULTIPLIER, + steps_per_epoch=STEPS_PER_EPOCH, + validation_steps=VALIDATION_SAMPLES // VALIDATION_BATCH_SIZE, + callbacks=callbacks, + verbose=2, + ) + + +if __name__ == "__main__": + train_and_evaluate()