From 02f4425ceadec0c10c1e9903286c1121a9313357 Mon Sep 17 00:00:00 2001 From: Vasil Zlatanov Date: Mon, 24 Jun 2019 21:31:41 +0100 Subject: Public FYP Release --- classifier-logo.py | 51 ++++ classifier.py | 48 ++++ densenet.py | 256 +++++++++++++++++ eval_utils.py | 293 +++++++++++++++++++ imagenet_input.py | 166 +++++++++++ logo_input.py | 143 ++++++++++ models.py | 387 ++++++++++++++++++++++++++ resnet50.py | 348 +++++++++++++++++++++++ resnet_preprocessing.py | 87 ++++++ util/__pycache__/dataset_utils.cpython-37.pyc | Bin 0 -> 4355 bytes util/dataset_utils.py | 150 ++++++++++ util/generate_smaller.sh | 22 ++ util/make-tfrecords.py | 203 ++++++++++++++ util/plot-report | 102 +++++++ util/plot-softmax | 94 +++++++ util/splitter | 30 ++ util/splitter-man | 30 ++ util/test.dump | 0 18 files changed, 2410 insertions(+) create mode 100755 classifier-logo.py create mode 100755 classifier.py create mode 100755 densenet.py create mode 100644 eval_utils.py create mode 100644 imagenet_input.py create mode 100644 logo_input.py create mode 100644 models.py create mode 100755 resnet50.py create mode 100644 resnet_preprocessing.py create mode 100644 util/__pycache__/dataset_utils.cpython-37.pyc create mode 100644 util/dataset_utils.py create mode 100755 util/generate_smaller.sh create mode 100755 util/make-tfrecords.py create mode 100755 util/plot-report create mode 100755 util/plot-softmax create mode 100755 util/splitter create mode 100755 util/splitter-man create mode 100644 util/test.dump diff --git a/classifier-logo.py b/classifier-logo.py new file mode 100755 index 0000000..87e5779 --- /dev/null +++ b/classifier-logo.py @@ -0,0 +1,51 @@ +#!/usr/bin/python + +from absl import flags +from absl import logging + +import os +import sys +import numpy as np +import tensorflow as tf +import models +import resnet_preprocessing +from tensorflow.keras.utils import plot_model + + +DEF_IMAGE_WIDTH = None +DEF_IMAGE_HEIGHT = None +DEF_WEIGHTS = 'weights.h5' + +flags.DEFINE_integer('image_width', DEF_IMAGE_WIDTH, '') +flags.DEFINE_integer('image_height', DEF_IMAGE_HEIGHT, '') +flags.DEFINE_string('weights', DEF_WEIGHTS, 'Weights of the model') +flags.DEFINE_integer('num_classes', 100, 'Number of classes thei weights were trained for') + +FLAGS = flags.FLAGS + +FLAGS(sys.argv) + +print(FLAGS.weights) + +classes = [ "absa_logo", "adobe_logo", "airbnb_logo", "alibaba_logo", "amazon_logo", "americanas_logo", "americanexpress_logo", "aol_logo", "apple_logo", "argenta_logo", "att_logo", "bancodechile_logo", "bancodecredito_logo", "bancodobrasil_logo", "bancosantander_logo", "bankofamerica_logo", "barclaysuk_logo", "bestchangecom_logo", "bet365_logo", "binance_logo", "blockchain_logo", "bnpparibas_logo", "bradesco_logo", "britishtelecom_logo", "caixabrazil_logo", "canadapharmacy_logo", "capitalone_logo", "casasbahia_logo", "chase_logo", "cibc_logo", "citicorp_logo", "coinbase_logo", "dailymirror_logo", "dhl_logo", "docusign_logo", "dropbox_logo", "ebay_logo", "ethereum_logo", "facebook_logo", "federalexpress_logo", "fonbetru_logo", "freefr_logo", "genericbankfraud_logo", "godaddy_logo", "google_logo", "grandlisboamacau_logo", "halkbankas_logo", "hmrevenuecustoms_logo", "hsbceub_logo", "impotsgouvfr_logo", "inggroup_logo", "instagram_logo", "interac_logo", "itauunibanco_logo", "lacaixaes_logo", "lassurancemaladie_logo", "lasvegassands_logo", "linkedin_logo", "lloydsbank_logo", "logos.txt", "luno_logo", "mcafee_logo", "mercadolibre_logo", "mercadopago_logo", "metrobank_uk_logo", "microsoft_logo", "mostbet_logo", "myetherwallet_logo", "nationalaustraliabank_logo", "natwest_logo", "netease_logo", "netflix_logo", "netseu_logo", "orange_logo", "ourtime_logo", "ovh_logo", "paypal_logo", "phpshell_logo", "posteitaliane_logo", "postmaster_logo", "rayban_logo", "rbc_logo", "scotiabank_logo", "standardchartered_logo", "steam_logo", "suntrust_logo", "swisscom_logo", "torontodominion_logo", "uber_logo", "unicredit_logo", "usaa_logo", "usbank_logo", "visa_logo", "vkontakte_logo", "walmart_logo", "wellsfargo_logo", "wetransfer_logo", "whatsapp_logo", "xfinity_logo", "yahoo_logo" ] + +model = models.get_logo_model(width=FLAGS.image_width, height=FLAGS.image_height, num_classes=FLAGS.num_classes) + +weights_file = os.path.join(FLAGS.weights) +model.load_weights(weights_file) +model.save_weights('new.hdf5') + +image_bytes = tf.read_file('my_logo.png') + +images_raw = resnet_preprocessing.preprocess_image(image_bytes, FLAGS.image_width, FLAGS.image_height, resize=False, is_training=True) + +#plot_model(model, to_file='model.pdf', show_layer_names=False, show_shapes=True) + +images_expanded = tf.expand_dims(images_raw, 0) +predictions = model.predict_on_batch(images_expanded) + +for prediction in predictions: + largest_ind = np.argpartition(prediction, -5)[-5:] + + for i in largest_ind[np.argsort(-prediction[largest_ind])]: + print(classes[i]+" : \t\t"+str(prediction[i])) diff --git a/classifier.py b/classifier.py new file mode 100755 index 0000000..b8e2b6a --- /dev/null +++ b/classifier.py @@ -0,0 +1,48 @@ +#!/usr/bin/python + +from absl import flags +from absl import logging + +import os +import sys +import numpy as np +import tensorflow as tf +import models + +import resnet_preprocessing + + +DEF_IMAGE_WIDTH = 320 +DEF_IMAGE_HEIGHT = 240 +DEF_WEIGHTS = 'weights.h5' + +flags.DEFINE_integer('image_width', DEF_IMAGE_WIDTH, '') +flags.DEFINE_integer('image_height', DEF_IMAGE_HEIGHT, '') +flags.DEFINE_string('weights', DEF_WEIGHTS, 'Weights of the model') +flags.DEFINE_integer('num_classes', 39, 'Number of classes thei weights were trained for') + +FLAGS = flags.FLAGS + +FLAGS(sys.argv) + +print(FLAGS.weights) + +classes = [ "adnetwork", "adobe", "airbnb", "amazon", "applecomputer", "applecomputer_scam", "bancosantander", "bankofamerica", "bnbankru", "bnpparibas", "chase", "craigslist", "dhl", "docusign", "dropbox", "facebook", "genericwebmailphishing", "godaddy", "google", "holding", "ingdirect", "linkedin", "microsoft", "microsoft_scam", "navyfederalcreditunion", "netflix", "orange", "paypal", "phpshell", "posteitaliane", "postmaster", "squarespace", "unicreditgroup", "visa", "vkontakte", "wellsfargo", "wetransfer", "windowslive", "yahoo"] + +model = models.ResNet50(width=FLAGS.image_width, height=FLAGS.image_height, num_classes=FLAGS.num_classes) + +weights_file = os.path.join(FLAGS.weights) +model.load_weights(weights_file) + +image_bytes = tf.read_file('my_file.png') + +images_raw = resnet_preprocessing.preprocess_image(image_bytes, FLAGS.image_width, FLAGS.image_height, resize=True, is_training=True) + +images_expanded = tf.expand_dims(images_raw, 0) +predictions = model.predict_on_batch(images_expanded) + +for prediction in predictions: + largest_ind = np.argpartition(prediction, -5)[-5:] + + for i in largest_ind[np.argsort(-prediction[largest_ind])]: + print(classes[i]+" : \t\t"+str(prediction[i])) diff --git a/densenet.py b/densenet.py new file mode 100755 index 0000000..22cf3cb --- /dev/null +++ b/densenet.py @@ -0,0 +1,256 @@ +#!/usr/bin/python3 +# Copyright 2018 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +r"""DenseNet implemented with Keras running on Cloud TPUs. + +This file shows how you can run DenseNet on a Cloud TPU using the TensorFlow +Keras support. This is configured for ImageNet (e.g. 1000 classes), but you can +easily adapt to your own datasets by changing the code appropriately. +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os + +from absl import app +from absl import flags +from absl import logging +import numpy as np +import tensorflow as tf + +import eval_utils +import imagenet_input +from models.densenet import DenseNetImageNet121 +from tensorflow.python.keras import backend as K +from tensorflow.python.keras.optimizer_v2 import gradient_descent, adam + +try: + import h5py as _ # pylint: disable=g-import-not-at-top + HAS_H5PY = True +except ImportError: + logging.warning('`h5py` is not installed. Please consider installing it ' + 'to save weights for long-running training.') + HAS_H5PY = False + + +# Imagenet training and test data sets. + +IMAGE_WIDTH = 320 +IMAGE_HEIGHT = 240 +EPOCHS = 90 # Standard imagenet training regime. + +# Training hyperparameters. +NUM_CORES = 8 +PER_CORE_BATCH_SIZE = 64 +CPU_BATCH_SIZE = 4 +BATCH_SIZE = NUM_CORES * PER_CORE_BATCH_SIZE +BASE_LEARNING_RATE = 0.4 +# Learning rate schedule +LR_SCHEDULE = [ # (multiplier, epoch to start) tuples + (1.0, 5), (0.1, 30), (0.01, 60), (0.001, 80) +] + +DEFAULT_WEIGHTS_H5 = 'resnet50_weights.h5' +DEFAULT_LOG_DIR = '/tmp/netcraft' +DEFAULT_BUCKET = 'gs://netcraft/' + +flags.DEFINE_integer('epochs', EPOCHS, '') +flags.DEFINE_string('weights', None, 'Use saved weights') +flags.DEFINE_string('bucket', DEFAULT_BUCKET, 'Bucket to use') +flags.DEFINE_string('tpu', None, 'Name of the TPU to use.') +flags.DEFINE_string('data', None, 'Path to training and testing data.') +flags.DEFINE_string( + 'log', DEFAULT_LOG_DIR, + ('The directory where the model weights and training/evaluation summaries ' + 'are stored. If not specified, save to /tmp/netcraft.')) +flags.DEFINE_bool( + 'complete_eval', True, + 'Eval both top 1 and top 5 accuracy. Otherwise, only eval top 1 accuracy. ' + 'Furthemore generate confusion matrixes and save softmax values in log_dir') +flags.DEFINE_bool('evalonly', False, 'Only run eval with given weights, do not train') + +FLAGS = flags.FLAGS + +def learning_rate_schedule(current_epoch, current_batch): + """Handles linear scaling rule, gradual warmup, and LR decay. + + The learning rate starts at 0, then it increases linearly per step. + After 5 epochs we reach the base learning rate (scaled to account + for batch size). + After 30, 60 and 80 epochs the learning rate is divided by 10. + After 90 epochs training stops and the LR is set to 0. This ensures + that we train for exactly 90 epochs for reproducibility. + + Args: + current_epoch: integer, current epoch indexed from 0. + current_batch: integer, current batch in the current epoch, indexed from 0. + + Returns: + Adjusted learning rate. + """ + return 0.0 + epoch = current_epoch + float(current_batch) / train_steps_per_epoch + warmup_lr_multiplier, warmup_end_epoch = LR_SCHEDULE[0] + if epoch < warmup_end_epoch: + # Learning rate increases linearly per step. + return BASE_LEARNING_RATE * warmup_lr_multiplier * epoch / warmup_end_epoch + for mult, start_epoch in LR_SCHEDULE: + if epoch >= start_epoch: + learning_rate = BASE_LEARNING_RATE * mult + else: + break + return learning_rate + + +class LearningRateBatchScheduler(tf.keras.callbacks.Callback): + """Callback to update learning rate on every batch (not epoch boundaries). + + N.B. Only support Keras optimizers, not TF optimizers. + + Args: + schedule: a function that takes an epoch index and a batch index as input + (both integer, indexed from 0) and returns a new learning rate as + output (float). + """ + + def __init__(self, schedule): + super(LearningRateBatchScheduler, self).__init__() + self.schedule = schedule + self.epochs = -1 + self.prev_lr = -1 + + def on_epoch_begin(self, epoch, logs=None): + if not hasattr(self.model.optimizer, 'lr'): + raise ValueError('Optimizer must have a "lr" attribute.') + self.epochs += 1 + + def on_batch_begin(self, batch, logs=None): + lr = self.schedule(self.epochs, batch) + if not isinstance(lr, (float, np.float32, np.float64)): + raise ValueError('The output of the "schedule" function should be float.') + if lr != self.prev_lr: + K.set_value(self.model.optimizer.lr, lr) + self.prev_lr = lr + logging.debug('Epoch %05d Batch %05d: LearningRateBatchScheduler change ' + 'learning rate to %s.', self.epochs, batch, lr) + + +def main(argv): + dinfo = np.load(os.path.join(FLAGS.data, 'dinfo.npz')) + classes = dinfo['classes'] + NUM_CLASSES = len(classes) + train_cnt = dinfo['train_cnt'] # 1141 # 50273 # Approximate number of images. + val_cnt = dinfo['val_cnt'] # 488 # 12560 # Number of images. + train_steps_per_epoch = int(train_cnt / BATCH_SIZE) + val_steps = int(val_cnt // BATCH_SIZE ) + + print("Using", train_cnt, "training images and", val_cnt, "for testing") + + logging.info('Building Keras DenseNet model') + model = DenseNetImageNet121(classes=NUM_CLASSES, weights=None) + + if FLAGS.tpu: + logging.info('Converting from CPU to TPU model.') + resolver = tf.contrib.cluster_resolver.TPUClusterResolver(tpu=FLAGS.tpu) + strategy = tf.contrib.tpu.TPUDistributionStrategy(resolver) + model = tf.contrib.tpu.keras_to_tpu_model(model, strategy=strategy) + + logging.info('Compiling model.') + model.compile( + optimizer=gradient_descent.SGD(learning_rate=BASE_LEARNING_RATE, momentum=0.9, nesterov=True), + loss='sparse_categorical_crossentropy', + metrics=['sparse_categorical_accuracy']) + + if FLAGS.data is None: + training_images = np.random.randn( + BATCH_SIZE, IMAGE_HEIGHT, IMAGE_WIDTH, 3).astype(np.float32) + training_labels = np.random.randint(NUM_CLASSES, size=BATCH_SIZE, + dtype=np.int32) + logging.info('Training model using synthetica data.') + model.fit( + training_images, + training_labels, + epochs=EPOCHS, + batch_size=BATCH_SIZE) + logging.info('Evaluating the model on synthetic data.') + model.evaluate(training_images, training_labels, verbose=0) + else: + per_core_batch_size = PER_CORE_BATCH_SIZE if FLAGS.tpu else CPU_BATCH_SIZE + imagenet_train = imagenet_input.ImageNetInput( + is_training=True, + data_dir=FLAGS.bucket+FLAGS.data if FLAGS.tpu else FLAGS.data, + per_core_batch_size=per_core_batch_size) + logging.info('Training model using real data in directory "%s".', + FLAGS.data) + # If evaluating complete_eval, we feed the inputs from a Python generator, + # so we need to build a single batch for all of the cores, which will be + # split on TPU. + per_core_batch_size = ( + BATCH_SIZE if FLAGS.complete_eval else PER_CORE_BATCH_SIZE) + imagenet_validation = imagenet_input.ImageNetInput( + is_training=False, + data_dir=FLAGS.bucket+FLAGS.data if FLAGS.tpu else FLAGS.data, + per_core_batch_size=per_core_batch_size) + + eval_callback = eval_utils.TensorBoardWithValidation( + log_dir=FLAGS.log, + validation_imagenet_input=imagenet_validation, + validation_steps=val_steps, + validation_epochs=[ 3, 10, 30, 60, 90], + write_images=True, + write_graph=True, + plot_wrong=True, + plot_cm=True, + plot_pr=True, + classes=classes, + complete_eval=FLAGS.complete_eval) + + callbacks = [ + LearningRateBatchScheduler(schedule=learning_rate_schedule), + eval_callback + ] + + if FLAGS.tpu: + model_in = imagenet_train.input_fn + else: + model_in = imagenet_train.input_fn() + + + if FLAGS.weights: + weights_file = os.path.join(FLAGS.weights) + logging.info('Loading model and weights from %s', weights_file) + model.load_weights(weights_file) + else: + weights_file = os.path.join(DEFAULT_WEIGHTS_H5) + + if FLAGS.evalonly: + eval_callback.set_model(model) + eval_callback.on_epoch_end(420) + else: + model.fit(model_in, + epochs=EPOCHS, + steps_per_epoch=train_steps_per_epoch, + callbacks=callbacks) + + logging.info('Saving weights into %s', weights_file) + model.save_weights(weights_file, overwrite=True) + + +if __name__ == '__main__': + tf.logging.set_verbosity(tf.logging.INFO) + app.run(main) diff --git a/eval_utils.py b/eval_utils.py new file mode 100644 index 0000000..bf00b0b --- /dev/null +++ b/eval_utils.py @@ -0,0 +1,293 @@ +# Copyright 2018 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Evaluation utils for `KerasTPUmodel`.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import numpy as np +from six.moves import xrange +import sys + +import tensorflow as tf +from tensorflow.python.keras import backend as K +from tensorflow.python.keras import optimizers +from tensorflow.python.keras import callbacks +from tensorflow.python.platform import tf_logging as logging +from sklearn.metrics import confusion_matrix + +import matplotlib.pyplot as plt +import io +import os +import itertools +import scikitplot as skplt + +from tqdm import trange + +def save_softmax(log_dir, epoch, labels, predictions): + location = os.path.join(log_dir, 'softmax' + str(epoch) + '.npz') + np.savez(location, labels=labels, predictions=predictions) + +def draw_graphs(self, log_dir, classes, y_true, y_probas, epoch): + y_pred = np.argmax(y_probas, axis=1) + if self._plot_cm: + skplt.metrics.plot_confusion_matrix(y_true, y_pred, normalize=True) + plot_to_tensorboard(log_dir, epoch, "model_projections", "confusion_matrix") + if self._plot_pr: + skplt.metrics.plot_precision_recall(y_true, y_probas) + plot_to_tensorboard(log_dir, epoch, "model_projections", "pr_curve") + +def plot_to_tensorboard(log_dir, epoch, model_projection, family_name): + buf = io.BytesIO() + plt.rcParams.update({'font.size': 5}) + plt.savefig(buf, dpi=250, format='png') + buf.seek(0) + image = tf.image.decode_png(buf.getvalue(), channels=3) + image = tf.expand_dims(image, 0) + + summary_op = tf.summary.image(model_projection, image, max_outputs=1, family=family_name) + writer = tf.summary.FileWriter(log_dir) + writer.add_summary(summary_op.eval(session=K.get_session()), epoch) + writer.close() + +def draw_c_matrix(log_dir, c_matrix, classes, epoch, normalize=False): + if normalize: + c_matrix = c_matrix.astype('float') / c_matrix.sum(axis=1)[:, np.newaxis] + plt.figure() + plt.imshow(c_matrix, cmap=plt.cm.Blues) + plt.xlabel('Predicted') + plt.ylabel('True Label') + tick_marks = np.arange(len(classes)) + plt.xticks(tick_marks, classes, rotation=45) + plt.yticks(tick_marks, classes) + + fmt = '.2f' + thresh = c_matrix.max() / 2. + for i, j in itertools.product(range(c_matrix.shape[0]), range(c_matrix.shape[1])): + plt.text(j, i, format(c_matrix[i, j], fmt), + horizontalalignment="center", + color="white" if c_matrix[i, j] > thresh else "black") + + buf = io.BytesIO() + plt.savefig(buf, dpi=500, format='png') + buf.seek(0) + image = tf.image.decode_png(buf.getvalue(), channels=4) + image = tf.expand_dims(image, 0) + + summary_op = tf.summary.image("model_projections", image, max_outputs=1, family='family_name') + writer = tf.summary.FileWriter(log_dir) + writer.add_summary(summary_op.eval(session=K.get_session()), epoch) + writer.close() + +def multi_top_k_accuracy(self, log_dir, model, evaluation_generator, eval_steps, classes, epoch, ks=(1, 5)): + """Calculates top k accuracy for the given `k` values. + + Args: + model: `KerasTPUModel` to evaluate. + evaluation_generator: a Python generator to generate (features, labels) for + evaluation. + eval_steps: int, number of evaluation steps. + ks: a tuple of int, position values to calculate top k accurary. + + Returns: + A dictionary containing top k accuracy for the given `k` values. + """ + def _count_matched(classes, predictions, labels, ks): + """Count number of pairs with label in any of top k predictions.""" + top_k_matched = dict.fromkeys(ks, 0) + for prediction, label in zip(predictions, labels): + for k in ks: + top_k_predictions = np.argpartition(prediction, -k)[-k:] + if label in top_k_predictions: + top_k_matched[k] += 1 + + return top_k_matched + + total = 0 + top_k_matched = dict.fromkeys(ks, 0) + c_matrix = np.zeros((len(classes),len(classes))) + all_labels = np.zeros((0,1)) + all_predictions = np.zeros((0,len(classes))) + logging.info('There are %d validation steps', eval_steps ) + t = trange(eval_steps) + for step in t: + try: + (features, labels) = next(evaluation_generator) + except Exception as e: + logging.debug(e) + break + predictions = model.predict_on_batch(features) # May be quicker + # predictions = model.predict(features, batch_size=8) + sorted_pred_args = np.flip(predictions.argsort(axis=1), axis=1) + flat_predictions = sorted_pred_args[:,0] + + # Todo: clean this function, it is a mess + + # Print some falsely predicted images + if self._plot_wrong:# and not (step+4) % 8: + # Squeeze labels into same dimension and type as predictions + sq_labels = np.squeeze(labels.astype(int)) + # If running in unity image per batch, squeeze squeezes one too many dimensions + if sq_labels.shape == (): + sq_labels = np.expand_dims(sq_labels, axis=0) + failed_indexes = np.where(np.not_equal(flat_predictions, sq_labels))[0] + limiter = 0 + for idx in failed_indexes: + if limiter > 90: + break + limiter += 1 + predicted_class_name = classes[flat_predictions[idx]] + true_class_name = classes[sq_labels[idx]] + proba_range = range(3) # Show softmax for top 3 + top_cl = classes[sorted_pred_args[idx][proba_range]] + probas = predictions[idx][sorted_pred_args[idx][proba_range]] + if probas[0] > 0.9: + top_3 = '\n'.join(cl + ": " + proba for cl, proba in zip(top_cl, probas.astype(str))) + print("Predicted", flat_predictions[idx], + "True:", sq_labels[idx], + "Proba:", probas.astype(str)) + plt.clf() + plt.imshow(features[idx].astype(int)) + plt.text(0, 0, top_3, size=9, va="bottom", bbox=dict(boxstyle="square", ec=(1., 0.5, 0.5), fc=(1., 0.8, 0.8),)) + plot_to_tensorboard(log_dir, epoch, "mislabled_images", "P_"+predicted_class_name+"_Tr_"+true_class_name) + + c_matrix += confusion_matrix(labels, flat_predictions, labels=range(len(classes))) + batch_top_k_matched = _count_matched(classes, predictions, labels, ks) + all_labels = np.vstack((all_labels, labels)) + all_predictions = np.vstack((all_predictions, predictions)) + for k, matched in batch_top_k_matched.items(): + top_k_matched[k] += matched + total += len(labels) + + t.set_description("Top 1: %f" % np.float_(top_k_matched[1]/float(total))) + + logging.info("Confusion matrix:") + print(c_matrix) + + try: + #draw_c_matrix(self._log_dir, c_matrix, self._targets, epoch, normalize=True) + #draw_c_matrix(self._log_dir, c_matrix, self._targets, epoch, normalize=False) + draw_graphs(self, log_dir, classes, all_labels, all_predictions, epoch) + except: + pass + + save_softmax(log_dir, epoch, all_labels, all_predictions) + metrics = dict([('top_{0}_accuracy'.format(k), np.float_(matched / float(total))) + for k, matched in top_k_matched.items()]) + print(metrics) + return metrics + + +class TensorBoardWithValidation(callbacks.TensorBoard): + """Extend TensorBoard Callback with validation . + + Validation is executed at the end of specified epochs, and the validation + metrics are exported to tensorboard for visualization. + + Args: + log_dir: the path of the directory where to save the log + files to be parsed by TensorBoard. + validation_imagenet_input: ImageNetInput for validation. + validation_steps: total number of steps to validate. + validation_epochs: a list of integers, epochs to run validation. + eval_top_k_accuracy: boolean, if true, evaluate top k accuracies using + multi_top_k_accuracy(). Otherwise, use model.evaluate(). + N.B. enabling this would significantly slow down the eval time due to + using python generator for evaluation input. + top_ks: a tuple of int, position values to calculate top k accurary. It's + only used when eval_top_k_accuracy is true. + """ + + def __init__(self, + log_dir, + validation_imagenet_input, + validation_steps, + validation_epochs, + write_graph, + write_images, + plot_wrong, + plot_cm, + plot_pr, + classes, + complete_eval, + top_ks=(1, 5)): + super(TensorBoardWithValidation, self).__init__(log_dir) + self._validation_imagenet_input = validation_imagenet_input + self._validation_steps = validation_steps + self._validation_epochs = validation_epochs + self._write_graph = write_graph + self._write_images = write_images + self._plot_wrong = plot_wrong + self._plot_cm = plot_cm + self._plot_pr = plot_pr + self._complete_eval = complete_eval + self._top_ks = top_ks + self._targets = classes + self._log_dir = log_dir + + def on_epoch_end(self, epoch, logs=None): + if epoch in self._validation_epochs: + logging.info('\nValidate in epoch %s', epoch) + if self._complete_eval: + logging.info("Running complete eval") + score = multi_top_k_accuracy( + self, + self._log_dir, + self.model, + self._validation_imagenet_input.evaluation_generator( + K.get_session()), + self._validation_steps, + self._targets, + epoch, + ks=self._top_ks) + for metric_name, metric_value in score.items(): + logs['val_' + metric_name] = metric_value + else: + # evaluate() is executed as callbacks during the training. In this case, + # _numpy_to_infeed_manager_list is not empty, so save it for + # recovery at the end of evaluate call. + # TODO(jingli): remove this monkey patch hack once the fix is included + # in future TF release. + original_numpy_to_infeed_manager_list = [] + if self.model._numpy_to_infeed_manager_list: + original_numpy_to_infeed_manager_list = ( + self.model._numpy_to_infeed_manager_list) + self.model._numpy_to_infeed_manager_list = [] + # Set _eval_function to None to enforce recompliation to use the newly + # created dataset in self._validation_imagenet_input.input_fn in + # evaluation. + # pylint: disable=bare-except + # pylint: disable=protected-access + try: + self.model._eval_function = None + except: + pass + + try: + # In TF 1.12, _eval_function does not exist, only test_function + # existed. + self.model.test_function = None + except: + pass + + scores = self.model.evaluate(self._validation_imagenet_input.input_fn, + steps=self._validation_steps) + self.model._numpy_to_infeed_manager_list = ( + original_numpy_to_infeed_manager_list) + for metric_name, metric_value in zip(self.model.metrics_names, scores): + logging.info('Evaluation metric. %s: %s.', metric_name, metric_value) + logs['val_' + metric_name] = metric_value + # The parent callback is responsible to write the logs as events file. + super(TensorBoardWithValidation, self).on_epoch_end(epoch, logs) diff --git a/imagenet_input.py b/imagenet_input.py new file mode 100644 index 0000000..55729b2 --- /dev/null +++ b/imagenet_input.py @@ -0,0 +1,166 @@ +# Copyright 2018 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Efficient ImageNet input pipeline using tf.data.Dataset.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os + +import tensorflow as tf + +import resnet_preprocessing + +class ImageNetInput(object): + """Generates ImageNet input_fn for training or evaluation. + + The training data is assumed to be in TFRecord format with keys as specified + in the dataset_parser below, sharded across 1024 files, named sequentially: + train-00000-of-01024 + train-00001-of-01024 + ... + train-01023-of-01024 + + The validation data is in the same format but sharded in 128 files. + + The format of the data required is created by the script at: + https://github.com/tensorflow/tpu/blob/master/tools/datasets/imagenet_to_gcs.py + + Args: + is_training: `bool` for whether the input is for training. + data_dir: `str` for the directory of the training and validation data; + if 'null' (the literal string 'null', not None), then construct a null + pipeline, consisting of empty images. + use_bfloat16: If True, use bfloat16 precision; else use float32. + per_core_batch_size: The per-TPU-core batch size to use. + """ + + def __init__(self, + width, + height, + resize, + is_training, + data_dir, + use_bfloat16=False, + per_core_batch_size=128): + self.image_preprocessing_fn = resnet_preprocessing.preprocess_image + self.is_training = is_training + self.width = width + self.height = height + self.resize = resize + self.use_bfloat16 = use_bfloat16 + self.data_dir = data_dir + if self.data_dir == 'null' or self.data_dir == '': + self.data_dir = None + self.per_core_batch_size = per_core_batch_size + + def dataset_parser(self, value): + """Parse an ImageNet record from a serialized string Tensor.""" + keys_to_features = { + 'image/encoded': + tf.FixedLenFeature((), tf.string, ''), + 'image/format': + tf.FixedLenFeature((), tf.string, 'png'), + 'image/class/label': + tf.FixedLenFeature([], tf.int64, -1), + 'image/height': + tf.FixedLenFeature([], tf.int64, -2), + 'image/width': + tf.FixedLenFeature([], tf.int64, -3), + + } + + parsed = tf.parse_single_example(value, keys_to_features) + image_bytes = tf.reshape(parsed['image/encoded'], shape=[]) + + image = self.image_preprocessing_fn( + image_bytes, + width=self.width, height=self.height, + resize=self.resize, + is_training=self.is_training, + use_bfloat16=self.use_bfloat16, + ) + + # Subtract one so that labels are in [0, 1000), and cast to float32 for + # Keras model. + label = tf.cast(tf.cast( + tf.reshape(parsed['image/class/label'], shape=[1]), dtype=tf.int32), # - 1, + dtype=tf.float32) + + return image, label + + def input_fn(self): + """Input function which provides a single batch for train or eval. + + Returns: + A `tf.data.Dataset` object. + """ + # Shuffle the filenames to ensure better randomization. + file_pattern = os.path.join( + self.data_dir, 'websites_train*' if self.is_training else 'websites_validation*') + dataset = tf.data.Dataset.list_files(file_pattern, shuffle=self.is_training) + + if self.is_training: + dataset = dataset.repeat() + + def fetch_dataset(filename): + buffer_size = 100 * 1024 * 1024 # 100 MiB per file + dataset = tf.data.TFRecordDataset(filename, buffer_size=buffer_size) + return dataset + + # Read the data from disk in parallel + dataset = dataset.interleave(fetch_dataset, cycle_length=16) + + if self.is_training: + dataset = dataset.shuffle(1024) + + # Parse, pre-process, and batch the data in parallel + dataset = dataset.apply( + tf.data.experimental.map_and_batch( + self.dataset_parser, + batch_size=self.per_core_batch_size, + num_parallel_batches=2, + drop_remainder=True)) + + # Prefetch overlaps in-feed with training + dataset = dataset.prefetch(tf.data.experimental.AUTOTUNE) + return dataset + + # TODO(xiejw): Remove this generator when we have support for top_k + # evaluation. + def evaluation_generator(self, sess): + """Creates a generator for evaluation.""" + next_batch = self.input_fn().make_one_shot_iterator().get_next() + while True: + try: + yield sess.run(next_batch) + except tf.errors.OutOfRangeError: + return + + def input_fn_null(self): + """Input function which provides null (black) images.""" + dataset = tf.data.Dataset.range(1).repeat().map(self._get_null_input) + dataset = dataset.prefetch(self.per_core_batch_size) + + dataset = dataset.batch(self.per_core_batch_size, drop_remainder=True) + + dataset = dataset.prefetch(32) # Prefetch overlaps in-feed with training + tf.logging.info('Input dataset: %s', str(dataset)) + return dataset + + def _get_null_input(self, _): + null_image = tf.zeros([320, 240, 3], tf.float32) + return null_image, tf.constant(0, tf.float32) diff --git a/logo_input.py b/logo_input.py new file mode 100644 index 0000000..1e017a4 --- /dev/null +++ b/logo_input.py @@ -0,0 +1,143 @@ +# Copyright 2018 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Efficient ImageNet input pipeline using tf.data.Dataset.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os + +import tensorflow as tf + +import resnet_preprocessing + +class ImageNetInput(object): + def __init__(self, + width, + height, + resize, + is_training, + data_dir, + use_bfloat16=False, + per_core_batch_size=128): + self.image_preprocessing_fn = resnet_preprocessing.preprocess_image + self.is_training = is_training + self.width = width + self.height = height + self.resize = resize + self.use_bfloat16 = use_bfloat16 + self.data_dir = data_dir + if self.data_dir == 'null' or self.data_dir == '': + self.data_dir = None + self.per_core_batch_size = per_core_batch_size + + def dataset_parser(self, value): + """Parse an ImageNet record from a serialized string Tensor.""" + keys_to_features = { + 'image/encoded': + tf.FixedLenFeature((), tf.string, ''), + 'image/format': + tf.FixedLenFeature((), tf.string, 'png'), + 'image/class/label': + tf.FixedLenFeature([], tf.int64, -1), + 'image/height': + tf.FixedLenFeature([], tf.int64, -2), + 'image/width': + tf.FixedLenFeature([], tf.int64, -3), + + } + + parsed = tf.parse_single_example(value, keys_to_features) + image_bytes = tf.reshape(parsed['image/encoded'], shape=[]) + + image = self.image_preprocessing_fn( + image_bytes, + width=self.width, height=self.height, + resize=self.resize, + is_training=self.is_training, + use_bfloat16=self.use_bfloat16, + ) + + # Subtract one so that labels are in [0, 1000), and cast to float32 for + # Keras model. + label = tf.cast(tf.cast( + tf.reshape(parsed['image/class/label'], shape=[1]), dtype=tf.int32), # - 1, + dtype=tf.float32) + + return image, label + + def input_fn(self): + """Input function which provides a single batch for train or eval. + + Returns: + A `tf.data.Dataset` object. + """ + # Shuffle the filenames to ensure better randomization. + file_pattern = os.path.join( + self.data_dir, 'websites_train*' if self.is_training else 'websites_validation*') + dataset = tf.data.Dataset.list_files(file_pattern, shuffle=self.is_training) + + if self.is_training: + dataset = dataset.repeat() + + def fetch_dataset(filename): + buffer_size = 100 * 1024 * 1024 # 100 MiB per file + dataset = tf.data.TFRecordDataset(filename, buffer_size=buffer_size) + return dataset + + # Read the data from disk in parallel + dataset = dataset.interleave(fetch_dataset, cycle_length=16) + + if self.is_training: + dataset = dataset.shuffle(1024) + + # Parse, pre-process, and batch the data in parallel + dataset = dataset.apply( + tf.data.experimental.map_and_batch( + self.dataset_parser, + batch_size=self.per_core_batch_size, + num_parallel_batches=2, + drop_remainder=True)) + + # Prefetch overlaps in-feed with training + dataset = dataset.prefetch(tf.data.experimental.AUTOTUNE) + return dataset + + # TODO(xiejw): Remove this generator when we have support for top_k + # evaluation. + def evaluation_generator(self, sess): + """Creates a generator for evaluation.""" + next_batch = self.input_fn().make_one_shot_iterator().get_next() + while True: + try: + yield sess.run(next_batch) + except tf.errors.OutOfRangeError: + return + + def input_fn_null(self): + """Input function which provides null (black) images.""" + dataset = tf.data.Dataset.range(1).repeat().map(self._get_null_input) + dataset = dataset.prefetch(self.per_core_batch_size) + + dataset = dataset.batch(self.per_core_batch_size, drop_remainder=True) + + dataset = dataset.prefetch(32) # Prefetch overlaps in-feed with training + tf.logging.info('Input dataset: %s', str(dataset)) + return dataset + + def _get_null_input(self, _): + null_image = tf.zeros([320, 240, 3], tf.float32) + return null_image, tf.constant(0, tf.float32) diff --git a/models.py b/models.py new file mode 100644 index 0000000..61cc26c --- /dev/null +++ b/models.py @@ -0,0 +1,387 @@ +# Copyright 2018 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""ResNet50 model for Keras. + +Adapted from tf.keras.applications.resnet50.ResNet50(). + +Related papers/blogs: +- https://arxiv.org/abs/1512.03385 +- https://arxiv.org/pdf/1603.05027v2.pdf +- http://torch.ch/blog/2016/02/04/resnets.html + +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import warnings + +import tensorflow as tf +from tensorflow.python.keras import layers +from tensorflow.python.keras import models +from tensorflow.python.keras import regularizers +from tensorflow.python.keras import utils + +import tensorflow.keras +from tensorflow.keras import backend as K +from tensorflow.keras.models import Sequential, Model +from tensorflow.keras.layers import Dense, Dropout, Activation, Flatten, Input, Lambda, Reshape +from tensorflow.keras.layers import Conv2D, MaxPooling2D, BatchNormalization, Conv2DTranspose, GlobalAveragePooling2D +from tensorflow.keras.layers import Input, concatenate + + +L2_WEIGHT_DECAY = 1e-4 +BATCH_NORM_DECAY = 0.9 +BATCH_NORM_EPSILON = 1e-5 + + +def identity_block(input_tensor, kernel_size, filters, stage, block, trainable=True): + """The identity block is the block that has no conv layer at shortcut. + + # Arguments + input_tensor: input tensor + kernel_size: default 3, the kernel size of + middle conv layer at main path + filters: list of integers, the filters of 3 conv layer at main path + stage: integer, current stage label, used for generating layer names + block: 'a','b'..., current block label, used for generating layer names + + # Returns + Output tensor for the block. + """ + filters1, filters2, filters3 = filters + if K.image_data_format() == 'channels_last': + bn_axis = 3 + else: + bn_axis = 1 + conv_name_base = 'res' + str(stage) + block + '_branch' + bn_name_base = 'bn' + str(stage) + block + '_branch' + + x = layers.Conv2D(filters1, (1, 1), + trainable=trainable, + kernel_initializer='he_normal', + kernel_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + bias_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + name=conv_name_base + '2a')(input_tensor) + x = layers.BatchNormalization(axis=bn_axis, + trainable=trainable, + momentum=BATCH_NORM_DECAY, + epsilon=BATCH_NORM_EPSILON, + name=bn_name_base + '2a')(x) + x = layers.Activation('relu')(x) + + x = layers.Conv2D(filters2, kernel_size, + trainable=trainable, + padding='same', + kernel_initializer='he_normal', + kernel_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + bias_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + name=conv_name_base + '2b')(x) + x = layers.BatchNormalization(axis=bn_axis, + trainable=trainable, + momentum=BATCH_NORM_DECAY, + epsilon=BATCH_NORM_EPSILON, + name=bn_name_base + '2b')(x) + x = layers.Activation('relu')(x) + + x = layers.Conv2D(filters3, (1, 1), + trainable=trainable, + kernel_initializer='he_normal', + kernel_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + bias_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + name=conv_name_base + '2c')(x) + x = layers.BatchNormalization(axis=bn_axis, + trainable=trainable, + momentum=BATCH_NORM_DECAY, + epsilon=BATCH_NORM_EPSILON, + name=bn_name_base + '2c')(x) + + x = layers.add([x, input_tensor]) + x = layers.Activation('relu')(x) + return x + + +def conv_block(input_tensor, + kernel_size, + filters, + stage, + block, + strides=(2, 2), + trainable=True): + """A block that has a conv layer at shortcut. + + # Arguments + input_tensor: input tensor + kernel_size: default 3, the kernel size of + middle conv layer at main path + filters: list of integers, the filters of 3 conv layer at main path + stage: integer, current stage label, used for generating layer names + block: 'a','b'..., current block label, used for generating layer names + strides: Strides for the second conv layer in the block. + + # Returns + Output tensor for the block. + + Note that from stage 3, + the second conv layer at main path is with strides=(2, 2) + And the shortcut should have strides=(2, 2) as well + """ + filters1, filters2, filters3 = filters + if K.image_data_format() == 'channels_last': + bn_axis = 3 + else: + bn_axis = 1 + conv_name_base = 'res' + str(stage) + block + '_branch' + bn_name_base = 'bn' + str(stage) + block + '_branch' + + x = layers.Conv2D(filters1, (1, 1), kernel_initializer='he_normal', + trainable=trainable, + kernel_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + bias_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + name=conv_name_base + '2a')(input_tensor) + x = layers.BatchNormalization(axis=bn_axis, + trainable=trainable, + momentum=BATCH_NORM_DECAY, + epsilon=BATCH_NORM_EPSILON, + name=bn_name_base + '2a')(x) + x = layers.Activation('relu')(x) + + x = layers.Conv2D(filters2, kernel_size, strides=strides, padding='same', + trainable=trainable, + kernel_initializer='he_normal', + kernel_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + bias_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + name=conv_name_base + '2b')(x) + x = layers.BatchNormalization(axis=bn_axis, + trainable=trainable, + momentum=BATCH_NORM_DECAY, + epsilon=BATCH_NORM_EPSILON, + name=bn_name_base + '2b')(x) + x = layers.Activation('relu')(x) + + x = layers.Conv2D(filters3, (1, 1), + trainable=trainable, + kernel_initializer='he_normal', + kernel_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + bias_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + name=conv_name_base + '2c')(x) + x = layers.BatchNormalization(axis=bn_axis, + trainable=trainable, + momentum=BATCH_NORM_DECAY, + epsilon=BATCH_NORM_EPSILON, + name=bn_name_base + '2c')(x) + + shortcut = layers.Conv2D(filters3, (1, 1), strides=strides, + trainable=trainable, + kernel_initializer='he_normal', + kernel_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + bias_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + name=conv_name_base + '1')(input_tensor) + shortcut = layers.BatchNormalization(axis=bn_axis, + trainable=trainable, + momentum=BATCH_NORM_DECAY, + epsilon=BATCH_NORM_EPSILON, + name=bn_name_base + '1')(shortcut) + + x = layers.add([x, shortcut]) + x = layers.Activation('relu')(x) + return x + + +def ResNet50(width, height, num_classes): + """Instantiates the ResNet50 architecture. + + Args: + num_classes: `int` number of classes for image classification. + + Returns: + A Keras model instance. + """ + # Determine proper input shape + if K.image_data_format() == 'channels_first': + input_shape = (3, height, width) + bn_axis = 1 + else: + input_shape = (height, width, 3) + bn_axis = 3 + + img_input = layers.Input(shape=input_shape) + x = layers.ZeroPadding2D(padding=(3, 3), name='conv1_pad')(img_input) + x = layers.Conv2D(64, (7, 7), + strides=(2, 2), + padding='valid', + kernel_initializer='he_normal', + kernel_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + bias_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + name='conv1')(x) + x = layers.BatchNormalization(axis=bn_axis, + momentum=BATCH_NORM_DECAY, + epsilon=BATCH_NORM_EPSILON, + name='bn_conv1')(x) + x = layers.Activation('relu')(x) + x = layers.ZeroPadding2D(padding=(1, 1), name='pool1_pad')(x) + x = layers.MaxPooling2D((3, 3), strides=(2, 2))(x) + + x = conv_block(x, 3, [64, 64, 256], stage=2, block='a', strides=(1, 1)) + x = identity_block(x, 3, [64, 64, 256], stage=2, block='b') + x = identity_block(x, 3, [64, 64, 256], stage=2, block='c') + + x = conv_block(x, 3, [128, 128, 512], stage=3, block='a') + x = identity_block(x, 3, [128, 128, 512], stage=3, block='b') + x = identity_block(x, 3, [128, 128, 512], stage=3, block='c') + x = identity_block(x, 3, [128, 128, 512], stage=3, block='d') + + x = conv_block(x, 3, [256, 256, 1024], stage=4, block='a') + x = identity_block(x, 3, [256, 256, 1024], stage=4, block='b') + x = identity_block(x, 3, [256, 256, 1024], stage=4, block='c') + x = identity_block(x, 3, [256, 256, 1024], stage=4, block='d') + x = identity_block(x, 3, [256, 256, 1024], stage=4, block='e') + x = identity_block(x, 3, [256, 256, 1024], stage=4, block='f') + + x = conv_block(x, 3, [512, 512, 2048], stage=5, block='a') + x = identity_block(x, 3, [512, 512, 2048], stage=5, block='b') + x = identity_block(x, 3, [512, 512, 2048], stage=5, block='c') + + x = layers.GlobalAveragePooling2D(name='avg_pool')(x) + + # When loading weights by name the last layer won't actually be loaded because + # the name depends on the number of classes + + x = layers.Dense( + num_classes, activation='softmax', + kernel_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + bias_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + name='fc'+str(num_classes))(x) + + # Create model. + return models.Model(img_input, x, name='resnet50') + +def get_logo_model(width, height, num_classes, output_layer = True, base_trainable = False): + if K.image_data_format() == 'channels_first': + input_shape = (3, height, width) + bn_axis = 1 + else: + input_shape = (height, width, 3) + bn_axis = 3 + + init_weights = tf.keras.initializers.he_normal() + + logo_model = Sequential() + logo_model.add(Conv2D(32, 3, padding='same', trainable = base_trainable, input_shape=input_shape, activation='elu', name='logo_conv1')) + + logo_model.add(Conv2D(32, 3, padding='same', trainable = base_trainable, use_bias = False, activation='elu', name='logo_conv2')) + + logo_model.add(Conv2D(64, 3, padding='same', trainable = base_trainable, strides=2, use_bias = False, activation='elu', name='logo_conv3')) + + logo_model.add(Conv2D(64, 3, padding='same', trainable = base_trainable, use_bias = False, activation='elu', name='logo_conv4')) + + logo_model.add(Conv2D(128, 3, padding='same', trainable = base_trainable, strides=2, use_bias = False, activation='elu', name='logo_conv5')) + + logo_model.add(Conv2D(128, 3, padding='same', trainable = True, use_bias = False, activation='elu', name='logo_conv6')) + logo_model.add(Dropout(0.3)) + + logo_model.add(Conv2D(128, 8, padding='same', trainable = True, use_bias = False, activation='elu', name='logo_conv7')) + + if output_layer: + # These two layers are only used in training + logo_model.add(GlobalAveragePooling2D(name='logo_avg_pool')) + logo_model.add(Dense( + num_classes, activation='softmax', name='logo_fc'+str(num_classes))) + + return logo_model + + +def get_logores_model(width, height, num_classes, resnet_trainable = True, logo_trainable = False, logo_end_trainable=True): + # Determine proper input shape + if K.image_data_format() == 'channels_first': + input_shape = (3, height, width) + bn_axis = 1 + else: + input_shape = (height, width, 3) + bn_axis = 3 + + img_input = layers.Input(shape=input_shape) + #logo_model = get_logo_model(width, height, num_classes, output_layer = False) + + + ## Freeze the weights of the logo model + #for layer in logo_model.layers: + # layer.trainable = False + + #logo_x = logo_model(img_input) + + logo_x = Conv2D(32, 3, padding='same', input_shape=input_shape, activation='elu', name='logo_conv1', trainable=logo_trainable)(img_input) + logo_x = Conv2D(32, 3, padding='same', use_bias = False, activation='elu', name='logo_conv2', trainable=logo_trainable)(logo_x) + logo_x = Conv2D(64, 3, padding='same', strides=2, use_bias = False, activation='elu', name='logo_conv3', trainable=logo_trainable)(logo_x) + logo_x = Conv2D(64, 3, padding='same', use_bias = False, activation='elu', name='logo_conv4', trainable=logo_trainable)(logo_x) + logo_x = Conv2D(128, 3, padding='same', strides=2, use_bias = False, activation='elu', name='logo_conv5', trainable=logo_trainable)(logo_x) + logo_x = Conv2D(128, 3, padding='same', use_bias = False, activation='elu', name='logo_conv6', trainable=logo_end_trainable)(logo_x) + logo_x = Dropout(0.3, trainable=logo_trainable)(logo_x) + logo_x = Conv2D(128, 8, padding='same', use_bias = False, activation='elu', name='logo_conv7', trainable=logo_end_trainable)(logo_x) + + x = layers.ZeroPadding2D(padding=(3, 3), name='conv1_pad')(img_input) + x = layers.Conv2D(64, (7, 7), + strides=(2, 2), + padding='valid', + kernel_initializer='he_normal', + kernel_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + bias_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + trainable=resnet_trainable, + name='conv1')(x) + x = layers.BatchNormalization(axis=bn_axis, + momentum=BATCH_NORM_DECAY, + epsilon=BATCH_NORM_EPSILON, + trainable=resnet_trainable, + name='bn_conv1')(x) + x = layers.Activation('relu')(x) + x = layers.ZeroPadding2D(padding=(1, 1), name='pool1_pad')(x) + x = layers.MaxPooling2D((3, 3), strides=(2, 2))(x) + + x = conv_block(x, 3, [64, 64, 256], stage=2, block='a', strides=(1, 1), trainable=resnet_trainable) + x = identity_block(x, 3, [64, 64, 256], stage=2, block='b', trainable=resnet_trainable) + x = identity_block(x, 3, [64, 64, 256], stage=2, block='c', trainable=resnet_trainable) + + x = conv_block(x, 3, [128, 128, 512], stage=3, block='a', trainable=resnet_trainable) + x = identity_block(x, 3, [128, 128, 512], stage=3, block='b', trainable=resnet_trainable) + x = identity_block(x, 3, [128, 128, 512], stage=3, block='c', trainable=resnet_trainable) + x = identity_block(x, 3, [128, 128, 512], stage=3, block='d', trainable=resnet_trainable) + + x = conv_block(x, 3, [256, 256, 1024], stage=4, block='a', trainable=resnet_trainable) + x = identity_block(x, 3, [256, 256, 1024], stage=4, block='b', trainable=resnet_trainable) + x = identity_block(x, 3, [256, 256, 1024], stage=4, block='c', trainable=resnet_trainable) + x = identity_block(x, 3, [256, 256, 1024], stage=4, block='d', trainable=resnet_trainable) + x = identity_block(x, 3, [256, 256, 1024], stage=4, block='e', trainable=resnet_trainable) + x = identity_block(x, 3, [256, 256, 1024], stage=4, block='f', trainable=resnet_trainable) + + x = conv_block(x, 3, [512, 512, 2048], stage=5, block='a', trainable=resnet_trainable) + x = identity_block(x, 3, [512, 512, 2048], stage=5, block='b', trainable=resnet_trainable) + x = identity_block(x, 3, [512, 512, 2048], stage=5, block='c', trainable=resnet_trainable) + + x = layers.GlobalAveragePooling2D(name='avg_pool')(x) + logo_x = layers.GlobalAveragePooling2D(name='logo_avg_pool')(logo_x) + + x = layers.concatenate([x, logo_x]) + + # When loading weights by name the last layer won't actually be loaded because + # the name depends on the number of classes + + x = layers.Dense( + num_classes, activation='softmax', + kernel_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + bias_regularizer=regularizers.l2(L2_WEIGHT_DECAY), + name='fc'+str(num_classes))(x) + + return models.Model(img_input, x, name='resnet50+logo') diff --git a/resnet50.py b/resnet50.py new file mode 100755 index 0000000..e063bfc --- /dev/null +++ b/resnet50.py @@ -0,0 +1,348 @@ +#!/usr/bin/python3 +# Copyright 2018 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +"""ResNet-50 implemented with Keras running on Cloud TPUs. + +This file shows how you can run ResNet-50 on a Cloud TPU using the TensorFlow +Keras support. This is configured for ImageNet (e.g. 1000 classes), but you can +easily adapt to your own datasets by changing the code appropriately. +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os + +from absl import app +from absl import flags +from absl import logging +import numpy as np +import tensorflow as tf + +import eval_utils +import imagenet_input +import models +from tensorflow.python.keras import backend as K +from tensorflow.python.keras.optimizer_v2 import gradient_descent, adam + +try: + import h5py as _ # pylint: disable=g-import-not-at-top + HAS_H5PY = True +except ImportError: + logging.warning('`h5py` is not installed. Please consider installing it ' + 'to save weights for long-running training.') + HAS_H5PY = False + + +# Imagenet training and test data sets. + +DEF_IMAGE_WIDTH = 320 +DEF_IMAGE_HEIGHT = 240 +DEF_EPOCHS = 90 # Standard imagenet training regime. + +# Training hyperparameters. +NUM_CORES = 8 +PER_CORE_BATCH_SIZE = 4 +CPU_BATCH_SIZE = 1 +BASE_LEARNING_RATE = 1e-3 +# Learning rate schedule +LR_SCHEDULE = [ # (multiplier, epoch to start) tuples + (1.0, 5), (0.1, 30), (0.01, 60), (0.001, 80) +] + +DEFAULT_WEIGHTS_H5 = 'resnet50_weights.h5' +DEFAULT_LOG_DIR = '/tmp/netcraft' +DEFAULT_BUCKET = 'gs://netcraft/' + +flags.DEFINE_float('lr', BASE_LEARNING_RATE, 'Defines the step size when training') +flags.DEFINE_integer('epochs', DEF_EPOCHS, 'Number of epochs until which to train') +flags.DEFINE_integer('split_epochs', 1, 'Split epochs into smaller bits, helps save weights') +flags.DEFINE_integer('initial_epoch', 0, 'Epoch from which to start, useful when resuming training') +flags.DEFINE_integer('image_width', DEF_IMAGE_WIDTH, '') +flags.DEFINE_integer('image_height', DEF_IMAGE_HEIGHT, '') +flags.DEFINE_string('weights', None, 'Use saved weights') +flags.DEFINE_string('weights2', None, 'Use another saved weights') +flags.DEFINE_string('bucket', DEFAULT_BUCKET, 'Bucket to use') +flags.DEFINE_string('tpu', None, 'Name of the TPU to use.') +flags.DEFINE_string('data', None, 'Path to training and testing data.') +flags.DEFINE_string('model', "resnet", 'Which logo to use (resnet, or logo)') +flags.DEFINE_string( + 'log', DEFAULT_LOG_DIR, + ('The directory where the model weights and training/evaluation summaries ' + 'are stored. If not specified, save to /tmp/netcraft.')) +flags.DEFINE_bool( + 'complete_eval', True, + 'Eval both top 1 and top 5 accuracy. Otherwise, only eval top 1 accuracy. ' + 'Furthemore generate confusion matrixes and save softmax values in log_dir') +flags.DEFINE_bool('evalonly', False, 'Only run eval with given weights, do not train') +flags.DEFINE_bool('class_weights', False, 'Use class weights to deal with imbalanced dataset') +flags.DEFINE_integer('benign_multiplier', 1, 'Multiplier for weigh tof benign class') +flags.DEFINE_bool('plot_wrong', False, 'Plot false images in tensorboard, make eval slower') +flags.DEFINE_bool('plot_cm', True, 'Plot confusion matrix in tensorboard') +flags.DEFINE_bool('plot_pr', True, 'Plot precision recall in tensorboard') +flags.DEFINE_bool('weights_by_name', False, 'Load weights by name, this allows loading weights with an incompatible fully '+ + 'connect layer i.e. a different number of targets. The FC layer is randomly initiated and needs to be trained.') + +FLAGS = flags.FLAGS + +def learning_rate_schedule(current_epoch, current_batch, train_steps_per_epoch, base_learning_rate): + """Handles linear scaling rule, gradual warmup, and LR decay. + + The learning rate starts at 0, then it increases linearly per step. + After 5 epochs we reach the base learning rate (scaled to account + for batch size). + After 30, 60 and 80 epochs the learning rate is divided by 10. + After 90 epochs training stops and the LR is set to 0. This ensures + that we train for exactly 90 epochs for reproducibility. + + Args: + current_epoch: integer, current epoch indexed from 0. + current_batch: integer, current batch in the current epoch, indexed from 0. + + Returns: + Adjusted learning rate. + """ + epoch = current_epoch + float(current_batch) / train_steps_per_epoch + warmup_lr_multiplier, warmup_end_epoch = LR_SCHEDULE[0] + if epoch < warmup_end_epoch: + # Learning rate increases linearly per step. + return base_learning_rate * warmup_lr_multiplier * epoch / warmup_end_epoch + for mult, start_epoch in LR_SCHEDULE: + if epoch >= start_epoch: + learning_rate = base_learning_rate * mult + else: + break + return learning_rate + + +class LearningRateBatchScheduler(tf.keras.callbacks.Callback): + """Callback to update learning rate on every batch (not epoch boundaries). + + N.B. Only support Keras optimizers, not TF optimizers. + + Args: + schedule: a function that takes an epoch index and a batch index as input + (both integer, indexed from 0) and returns a new learning rate as + output (float). + """ + + def __init__(self, schedule, train_steps_per_epoch, base_learning_rate): + super(LearningRateBatchScheduler, self).__init__() + self.base_lr = base_learning_rate + self.schedule = schedule + self.train_steps_per_epoch = train_steps_per_epoch + self.epochs = -1 + self.prev_lr = -1 + + def on_epoch_begin(self, epoch, logs=None): + if not hasattr(self.model.optimizer, 'lr'): + raise ValueError('Optimizer must have a "lr" attribute.') + self.epochs += 1 + + def on_batch_begin(self, batch, logs=None): + lr = self.schedule(self.epochs, batch, self.train_steps_per_epoch, self.base_lr) + if not isinstance(lr, (float, np.float32, np.float64)): + raise ValueError('The output of the "schedule" function should be float.') + if lr != self.prev_lr: + K.set_value(self.model.optimizer.lr, lr) + self.prev_lr = lr + logging.debug('Epoch %05d Batch %05d: LearningRateBatchScheduler change ' + 'learning rate to %s.', self.epochs, batch, lr) + + +def main(argv): + if FLAGS.data: + dinfo = np.load(os.path.join(FLAGS.data, 'dinfo.npz'), allow_pickle=True) + classes = dinfo['classes'] + num_classes = len(classes) + train_cnt = dinfo['train_cnt'] # 1141 # 50273 # Approximate number of images. + val_cnt = dinfo['val_cnt'] # 488 # 12560 # Number of images. + class_weights = dinfo['class_weights'].tolist() + #class_weights = class_weights[()] # Unpack 0d np.array + + if FLAGS.class_weights and FLAGS.benign_multiplier != 1: + benign_class = np.squeeze(np.where(classes=='benign')) + if benign_class: + benign_class = np.asscalar(benign_class) + class_weights[benign_class] *= FLAGS.benign_multiplier + else: + logging.warning("Could not find benign class. Ignoring benign multiplier.") + else: + train_cnt = 10e5 + val_cnt = 10e4 + num_classes = 10e2 + + if FLAGS.tpu: + batch_size = NUM_CORES * PER_CORE_BATCH_SIZE + else: + batch_size = CPU_BATCH_SIZE + + train_steps_per_epoch = int(train_cnt / (batch_size * FLAGS.split_epochs)) + val_steps = int(val_cnt // batch_size ) + + logging.info("Using %d training images and %d for validation", train_cnt, val_cnt) + + if FLAGS.model == 'resnet': + logging.info('Building Keras ResNet-50 model') + model = models.ResNet50(width=FLAGS.image_width, height=FLAGS.image_height, num_classes=num_classes) + elif FLAGS.model == 'combined': + logging.info('Building Keras ResNet-50 + LOGO model') + model = models.get_logores_model(width=FLAGS.image_width, height=FLAGS.image_height, num_classes=num_classes, resnet_trainable=False) + elif FLAGS.model == 'combined_trainable': + logging.info('Building Keras ResNet-50 + LOGO model') + model = models.get_logores_model(width=FLAGS.image_width, height=FLAGS.image_height, num_classes=num_classes, resnet_trainable=True) + elif FLAGS.model == 'logo': + logging.info('Building LogoNet model') + model = models.get_logo_model(width=None, height=None, num_classes=num_classes, base_trainable=True) + elif FLAGS.model == 'logo_extended': + logging.info('Building LogoNet model') + model = models.get_logo_model(width=FLAGS.image_width, height=FLAGS.image_height, base_trainable=False, num_classes=num_classes) + elif FLAGS.model == 'logo_new': + logging.info('Building LogoNet model') + model = models.get_logo_model_new(width=FLAGS.image_width, height=FLAGS.image_height, base_trainable=False, num_classes=num_classes) + elif FLAGS.model == 'logo_extended_trainable': + logging.info('Building LogoNet model') + model = models.get_logo_model(width=FLAGS.image_width, height=FLAGS.image_height, base_trainable=True, num_classes=num_classes) + else: + return 'Only valid models are resnet and logo' + + if FLAGS.tpu: + logging.info('Converting from CPU to TPU model.') + resolver = tf.contrib.cluster_resolver.TPUClusterResolver(tpu=FLAGS.tpu) + strategy = tf.contrib.tpu.TPUDistributionStrategy(resolver) + model = tf.contrib.tpu.keras_to_tpu_model(model, strategy=strategy) + + logging.info('Compiling model.') + model.compile( + optimizer=adam.Adam(learning_rate=FLAGS.lr), + loss='sparse_categorical_crossentropy', + metrics=['sparse_categorical_accuracy']) + + if FLAGS.data is None: + training_images = np.random.randn( + batch_size, FLAGS.image_height, FLAGS.image_width, 3).astype(np.float32) + training_labels = np.random.randint(num_classes, size=batch_size, + dtype=np.int32) + logging.info('Training model using synthetica data, use --data flag to provided real data.') + model.fit( + training_images, + training_labels, + epochs=FLAGS.epochs, + initial_epoch=FLAGS.initial_epoch, + batch_size=batch_size) + logging.info('Evaluating the model on synthetic data.') + model.evaluate(training_images, training_labels, verbose=0) + else: + per_core_batch_size = PER_CORE_BATCH_SIZE if FLAGS.tpu else CPU_BATCH_SIZE + imagenet_train = imagenet_input.ImageNetInput( + width=FLAGS.image_width, + height=FLAGS.image_height, + resize=False if (FLAGS.model == 'logo') else True, + is_training=True, + data_dir=FLAGS.bucket+FLAGS.data if FLAGS.tpu else FLAGS.data, + per_core_batch_size=per_core_batch_size) + logging.info('Training model using real data in directory "%s".', + FLAGS.data) + # If evaluating complete_eval, we feed the inputs from a Python generator, + # so we need to build a single batch for all of the cores, which will be + # split on TPU. + per_core_batch_size = ( + batch_size if (FLAGS.complete_eval or not FLAGS.tpu) else PER_CORE_BATCH_SIZE) + imagenet_validation = imagenet_input.ImageNetInput( + FLAGS.image_width, FLAGS.image_height, + resize=False if (FLAGS.model == 'logo') else True, + is_training=False, + data_dir=FLAGS.bucket+FLAGS.data if FLAGS.tpu else FLAGS.data, + per_core_batch_size=per_core_batch_size) + + if FLAGS.evalonly: + validation_epochs= [420] + logging.info("Only running a single validation epoch") + else: + validation_epochs=[ 3, 10, 30, 60, 90] + logging.info("Validation will be run on epochs %s", str(validation_epochs)) + + eval_callback = eval_utils.TensorBoardWithValidation( + log_dir=FLAGS.log, + validation_imagenet_input=imagenet_validation, + validation_steps=val_steps, + validation_epochs=validation_epochs, + write_images=True, + write_graph=True, + plot_wrong=FLAGS.plot_wrong, + plot_cm=FLAGS.plot_cm, + plot_pr=FLAGS.plot_pr, + classes=classes, + complete_eval=FLAGS.complete_eval) + + callbacks = [ + tf.keras.callbacks.ModelCheckpoint(FLAGS.log+"/weights.{epoch:02d}-{sparse_categorical_accuracy:.2f}.hdf5", + monitor='sparse_categorical_accuracy', verbose=1, + save_best_only=True, save_weights_only=True, mode='auto'), + LearningRateBatchScheduler(schedule=learning_rate_schedule, train_steps_per_epoch=train_steps_per_epoch, base_learning_rate=FLAGS.lr), + eval_callback + ] + + if FLAGS.tpu: + model_in = imagenet_train.input_fn + else: + model_in = imagenet_train.input_fn() + + preloaded_weights = [] + for layer in model.layers: + preloaded_weights.append(layer.get_weights()) + + if FLAGS.weights: + weights_file = os.path.join(FLAGS.weights) + logging.info('Loading trained weights from %s', weights_file) + model.load_weights(weights_file, by_name=FLAGS.weights_by_name) + if FLAGS.weights2: + weights2_file = os.path.join(FLAGS.weights2) + logging.info('Loading secondary trained weights from %s', weights2_file) + model.load_weights(weights2_file, by_name=FLAGS.weights_by_name) + else: + if FLAGS.weights2: + logging.debug("Ignoring --weights2 flag as no --weights") + weights_file = os.path.join(DEFAULT_WEIGHTS_H5) + + # Check if we loaded weights + for layer, pre in zip(model.layers, preloaded_weights): + weights = layer.get_weights() + + populated=True + if weights: + for weight, pr in zip(weights, pre): + if np.array_equal(weight, pr): + populated=False + + if not populated: + logging.warning('Layer %s not populated with weights!', layer.name) + + if FLAGS.evalonly: + eval_callback.set_model(model) + eval_callback.on_epoch_end(420) + else: + model.fit(model_in, + epochs=FLAGS.epochs, + initial_epoch=FLAGS.initial_epoch, + class_weight = class_weights if FLAGS.class_weights else None, + steps_per_epoch=train_steps_per_epoch, + callbacks=callbacks) + +if __name__ == '__main__': + tf.logging.set_verbosity(tf.logging.INFO) + app.run(main) diff --git a/resnet_preprocessing.py b/resnet_preprocessing.py new file mode 100644 index 0000000..72c799a --- /dev/null +++ b/resnet_preprocessing.py @@ -0,0 +1,87 @@ +# Trimmed by Vasil Zlatanov +# Copyright 2018 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""ImageNet preprocessing for ResNet.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import tensorflow as tf + +def resize_or_crop_image(image, target_height, target_width): + image_height = tf.shape(image)[0] + image_width = tf.shape(image)[1] + # If the viewport is long but the width is right simply crop the length of the page + # Otherwise we just resize the image bilinearly + image = tf.cond( + tf.logical_and(tf.greater(image_height, target_height),tf.equal(target_width, image_width)), + lambda: tf.cast(tf.image.crop_to_bounding_box(image, 0, 0, target_height, target_width),dtype=tf.float32), + lambda: tf.image.resize_images(image, [target_height,target_width], align_corners=True) + ) + return image + +def preprocess_for_train(image_bytes, target_width, target_height, resize, use_bfloat16): + """Preprocesses the given image for evaluation. + + Args: + image_bytes: `Tensor` representing an image binary of arbitrary size. + use_bfloat16: `bool` for whether to use bfloat16. + + Returns: + A preprocessed image `Tensor`. + """ + image = tf.image.decode_png(image_bytes, channels=3) + if resize: + image = resize_or_crop_image(image, target_height, target_width) + else: + image = tf.cast(image, tf.float32) + + return image + + +def preprocess_for_eval(image_bytes, target_width, target_height, resize, use_bfloat16): + """Preprocesses the given image for evaluation. + + Args: + image_bytes: `Tensor` representing an image binary of arbitrary size. + use_bfloat16: `bool` for whether to use bfloat16. + + Returns: + A preprocessed image `Tensor`. + """ + image = tf.image.decode_png(image_bytes, channels=3) + if resize: + image = resize_or_crop_image(image, target_height, target_width) + else: + image = tf.cast(image, tf.float32) + + return image + + +def preprocess_image(image_bytes, width, height, resize, is_training=False, use_bfloat16=False): + """Preprocesses the given image. + + Args: + image_bytes: `Tensor` representing an image binary of arbitrary size. + is_training: `bool` for whether the preprocessing is for training. + use_bfloat16: `bool` for whether to use bfloat16. + + Returns: + A preprocessed image `Tensor`. + """ + if is_training: + return preprocess_for_train(image_bytes, width, height, resize, use_bfloat16) + else: + return preprocess_for_eval(image_bytes, width, height, resize, use_bfloat16) diff --git a/util/__pycache__/dataset_utils.cpython-37.pyc b/util/__pycache__/dataset_utils.cpython-37.pyc new file mode 100644 index 0000000..7ed2852 Binary files /dev/null and b/util/__pycache__/dataset_utils.cpython-37.pyc differ diff --git a/util/dataset_utils.py b/util/dataset_utils.py new file mode 100644 index 0000000..fdaefca --- /dev/null +++ b/util/dataset_utils.py @@ -0,0 +1,150 @@ +# Copyright 2016 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Contains utilities for downloading and converting datasets.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import sys +import tarfile + +from six.moves import urllib +import tensorflow as tf + +LABELS_FILENAME = 'labels.txt' + + +def int64_feature(values): + """Returns a TF-Feature of int64s. + + Args: + values: A scalar or list of values. + + Returns: + A TF-Feature. + """ + if not isinstance(values, (tuple, list)): + values = [values] + return tf.train.Feature(int64_list=tf.train.Int64List(value=values)) + + +def bytes_feature(values): + """Returns a TF-Feature of bytes. + + Args: + values: A string. + + Returns: + A TF-Feature. + """ + return tf.train.Feature(bytes_list=tf.train.BytesList(value=[values])) + + +def float_feature(values): + """Returns a TF-Feature of floats. + + Args: + values: A scalar of list of values. + + Returns: + A TF-Feature. + """ + if not isinstance(values, (tuple, list)): + values = [values] + return tf.train.Feature(float_list=tf.train.FloatList(value=values)) + + +def image_to_tfexample(image_data, image_format, height, width, class_id): + return tf.train.Example(features=tf.train.Features(feature={ + 'image/encoded': bytes_feature(image_data), + 'image/format': bytes_feature(image_format), + 'image/class/label': int64_feature(class_id), + 'image/height': int64_feature(height), + 'image/width': int64_feature(width), + })) + + +def download_and_uncompress_tarball(tarball_url, dataset_dir): + """Downloads the `tarball_url` and uncompresses it locally. + + Args: + tarball_url: The URL of a tarball file. + dataset_dir: The directory where the temporary files are stored. + """ + filename = tarball_url.split('/')[-1] + filepath = os.path.join(dataset_dir, filename) + + def _progress(count, block_size, total_size): + sys.stdout.write('\r>> Downloading %s %.1f%%' % ( + filename, float(count * block_size) / float(total_size) * 100.0)) + sys.stdout.flush() + filepath, _ = urllib.request.urlretrieve(tarball_url, filepath, _progress) + print() + statinfo = os.stat(filepath) + print('Successfully downloaded', filename, statinfo.st_size, 'bytes.') + tarfile.open(filepath, 'r:gz').extractall(dataset_dir) + + +def write_label_file(labels_to_class_names, dataset_dir, + filename=LABELS_FILENAME): + """Writes a file with the list of class names. + + Args: + labels_to_class_names: A map of (integer) labels to class names. + dataset_dir: The directory in which the labels file should be written. + filename: The filename where the class names are written. + """ + labels_filename = os.path.join(dataset_dir, filename) + with tf.gfile.Open(labels_filename, 'w') as f: + for label in labels_to_class_names: + class_name = labels_to_class_names[label] + f.write('%d:%s\n' % (label, class_name)) + + +def has_labels(dataset_dir, filename=LABELS_FILENAME): + """Specifies whether or not the dataset directory contains a label map file. + + Args: + dataset_dir: The directory in which the labels file is found. + filename: The filename where the class names are written. + + Returns: + `True` if the labels file exists and `False` otherwise. + """ + return tf.gfile.Exists(os.path.join(dataset_dir, filename)) + + +def read_label_file(dataset_dir, filename=LABELS_FILENAME): + """Reads the labels file and returns a mapping from ID to class name. + + Args: + dataset_dir: The directory in which the labels file is found. + filename: The filename where the class names are written. + + Returns: + A map from a label (integer) to class name. + """ + labels_filename = os.path.join(dataset_dir, filename) + with tf.gfile.Open(labels_filename, 'rb') as f: + lines = f.read().decode() + lines = lines.split('\n') + lines = filter(None, lines) + + labels_to_class_names = {} + for line in lines: + index = line.index(':') + labels_to_class_names[int(line[:index])] = line[index+1:] + return labels_to_class_names diff --git a/util/generate_smaller.sh b/util/generate_smaller.sh new file mode 100755 index 0000000..68769b4 --- /dev/null +++ b/util/generate_smaller.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +OUTPUT_DIR='better_noise_logos' + +for width in $(seq 16 1 96);do + echo "Making width of $width" + for brand in $1/*;do + mkdir -p "$OUTPUT_DIR/$brand" + + find $brand -iname '*.png' | while read image;do + BRI=$((80 + RANDOM % 40)) + SAT=$((80 + RANDOM % 40)) + HUE=$((60 + RANDOM % 80)) + width_off=$((RANDOM % (320-width))) + height_off=$((RANDOM % (240-width))) + convert -size 320x240 xc: +noise Random $image -modulate $BRI,$SAT,$HUE -geometry ${width}x+${width_off}+${height_off} -composite "$OUTPUT_DIR/$brand/`basename $image`.$width.png" + sleep 1 + done + done + +done + diff --git a/util/make-tfrecords.py b/util/make-tfrecords.py new file mode 100755 index 0000000..f9f3e71 --- /dev/null +++ b/util/make-tfrecords.py @@ -0,0 +1,203 @@ +#!/usr/bin/python +import random +import os +import sys +import math +import tensorflow as tf +import dataset_utils +import numpy as np + +#===============DEFINE YOUR ARGUMENTS============== +flags = tf.app.flags + +#State your dataset directory +flags.DEFINE_string('data', None, 'String: Your dataset directory') + +# The number of images in the validation set. You would have to know the total number of examples in advance. This is essentially your evaluation dataset. +flags.DEFINE_float('validation_size', 0.25, 'Float: The proportion of examples in the dataset to be used for validation') + +# The number of shards per dataset split. +flags.DEFINE_integer('num_shards', 1, 'Int: Number of shards to split the TFRecord files') + +# Seed for repeatability. +flags.DEFINE_integer('random_seed', 0, 'Int: Random seed to use for repeatability.') +flags.DEFINE_bool('overwrite', False, 'Overwrite prevoiusly generated files') + +FLAGS = flags.FLAGS + +class ImageReader(object): + """Helper class that provides TensorFlow image coding utilities.""" + + def __init__(self): + # Initializes function that decodes RGB JPEG data. + self._decode_png_data = tf.placeholder(dtype=tf.string) + self._decode_png = tf.image.decode_png(self._decode_png_data, channels=0) + + def read_image_dims(self, sess, image_data): + image = self.decode_png(sess, image_data) + return image.shape[0], image.shape[1] + + def decode_png(self, sess, image_data): + image = sess.run(self._decode_png, + feed_dict={self._decode_png_data: image_data}) + assert len(image.shape) == 3 + return image + +def _get_filenames_and_classes(data): + """Returns a list of filenames and inferred class names. + + Args: + data: A directory containing a set of subdirectories representing + class names. Each subdirectory should contain PNG or JPG encoded images. + + Returns: + A list of image file paths, relative to `data` and the list of + subdirectories, representing class names. + """ + directories = [] + class_names = [] + for filename in os.listdir(data): + path = os.path.join(data, filename) + if os.path.isdir(path): + print(path) + directories.append(path) + class_names.append(filename) + + photo_filenames = [] + for directory in directories: + for filename in os.listdir(directory): + path = os.path.join(directory, filename) + photo_filenames.append(path) + + return photo_filenames, sorted(class_names) + + +def _get_dataset_filename(data, split_name, shard_id, _NUM_SHARDS): + output_filename = 'websites_%s_%05d-of-%05d.tfrecord' % ( + split_name, shard_id, _NUM_SHARDS) + return os.path.join(data, output_filename) + + +def _convert_dataset(split_name, filenames, class_names_to_ids, data, _NUM_SHARDS): + """Converts the given filenames to a TFRecord dataset. + + Args: + split_name: The name of the dataset, either 'train' or 'validation'. + filenames: A list of absolute paths to png or jpg images. + class_names_to_ids: A dictionary from class names (strings) to ids + (integers). + data: The directory where the converted datasets are stored. + """ + assert split_name in ['train', 'validation'] + + failed = 0 + success = 0 + # class_cnts is used for balancing training through class_weights + class_cnts = [0] * len(class_names_to_ids) + num_per_shard = int(math.ceil(len(filenames) / float(_NUM_SHARDS))) + + with tf.Graph().as_default(): + image_reader = ImageReader() + + with tf.Session('') as sess: + + for shard_id in range(_NUM_SHARDS): + output_filename = _get_dataset_filename( + data, split_name, shard_id, _NUM_SHARDS) + + with tf.python_io.TFRecordWriter(output_filename) as tfrecord_writer: + start_ndx = shard_id * num_per_shard + end_ndx = min((shard_id+1) * num_per_shard, len(filenames)) + for i in range(start_ndx, end_ndx): +# sys.stdout.write('\r>> Converting image %d/%d shard %d: %s' % ( +# i+1, len(filenames), shard_id, filenames[i])) +# sys.stdout.flush() + + # Read the filename: + image_data = tf.gfile.FastGFile(filenames[i], 'rb').read() + try: + height, width = image_reader.read_image_dims(sess, image_data) + class_name = os.path.basename(os.path.dirname(filenames[i])) + class_id = class_names_to_ids[class_name] + + example = dataset_utils.image_to_tfexample( + image_data, b'png', height, width, class_id) + tfrecord_writer.write(example.SerializeToString()) + success += 1; + class_cnts[class_id] += 1; + except: + failed = failed + 1; + + + + + sys.stdout.write('%d in total failed!\n' % failed) + sys.stdout.write('%d in total were written successfuly!\n' % success) + sys.stdout.flush() + return class_cnts + + +def _dataset_exists(data, _NUM_SHARDS): + for split_name in ['train', 'validation']: + for shard_id in range(_NUM_SHARDS): + output_filename = _get_dataset_filename( + data, split_name, shard_id, _NUM_SHARDS) + if not tf.gfile.Exists(output_filename): + return False + return True + +def main(): + + #=============CHECKS============== + #Check if there is a dataset directory entered + if not FLAGS.data: + raise ValueError('data is empty. Please state a data argument.') + + #If the TFRecord files already exist in the directory, then exit without creating the files again + if not FLAGS.overwrite and _dataset_exists(data = FLAGS.data, _NUM_SHARDS = FLAGS.num_shards): + print('Dataset files already exist. Exiting without re-creating them.') + print('Use --overwrite flag or remove them') + return None + #==========END OF CHECKS============ + + #Get a list of photo_filenames like ['123.jpg', '456.jpg'...] and a list of sorted class names from parsing the subdirectories. + photo_filenames, class_names = _get_filenames_and_classes(FLAGS.data) + + #Refer each of the class name to a specific integer number for predictions later + class_names_to_ids = dict(zip(class_names, range(len(class_names)))) + + #Find the number of validation examples we need + num_validation = int(FLAGS.validation_size * len(photo_filenames)) + + # Divide the training datasets into train and test: + random.seed(FLAGS.random_seed) + random.shuffle(photo_filenames) + training_filenames = photo_filenames[num_validation:] + validation_filenames = photo_filenames[:num_validation] + + # First, convert the training and validation sets. + train_cnts = _convert_dataset('train', training_filenames, class_names_to_ids, + data = FLAGS.data, _NUM_SHARDS = 1) + val_cnts = _convert_dataset('validation', validation_filenames, class_names_to_ids, + data = FLAGS.data, _NUM_SHARDS = 1) + + # Finally, write the labels file: + labels_to_class_names = dict(zip(range(len(class_names)), class_names)) + dataset_utils.write_label_file(labels_to_class_names, FLAGS.data) + + total_train_cnt = sum(train_cnts) + class_cnt = len(train_cnts) + class_weights = [ total_train_cnt/(train_cnts[i]*class_cnt+1e-10) for i in range(class_cnt) ] + + data_info = os.path.join(FLAGS.data, 'dinfo.npz') + np.savez(data_info, train_cnt=total_train_cnt, + val_cnt=sum(val_cnts), + class_weights=class_weights, + classes=class_names + ) + + print('\nFinished converting the dataset!') + +if __name__ == "__main__": + main() + diff --git a/util/plot-report b/util/plot-report new file mode 100755 index 0000000..927437f --- /dev/null +++ b/util/plot-report @@ -0,0 +1,102 @@ +#!/usr/bin/python +import tensorflow as tf +import numpy as np +import matplotlib.pyplot as plt +import scikitplot as skplt + +from sklearn.preprocessing import label_binarize +from sklearn.preprocessing import LabelEncoder +from sklearn.metrics import auc, confusion_matrix +from sklearn.metrics import precision_recall_curve +from sklearn.metrics import average_precision_score +from sklearn.metrics import classification_report + +flags = tf.app.flags + +flags.DEFINE_string('softmax', None, 'The softmax.npz file contained labels and probas') +flags.DEFINE_string('dinfo', None, 'The dinfo.npz file') +flags.DEFINE_integer('chunks', 4, 'The number of plots to produce') + + +FLAGS = flags.FLAGS + + +def plot_classification_report(classification_report, title='Classification report ', cmap='RdBu'): + ''' + Plot scikit-learn classification report. + Extension based on https://stackoverflow.com/a/31689645/395857 + ''' + lines = classification_report.split('\n') + + classes = [] + plotMat = [] + support = [] + class_names = [] + for line in lines[2 : (len(lines) - 2)]: + t = line.strip().split() + if len(t) < 2: continue + classes.append(t[0]) + v = [float(x) for x in t[1: len(t) - 1]] + support.append(int(t[-1])) + class_names.append(t[0]) + print(v) + plotMat.append(v) + + print('plotMat: {0}'.format(plotMat)) + print('support: {0}'.format(support)) + + xlabel = 'Metrics' + ylabel = 'Classes' + xticklabels = ['Precision', 'Recall', 'F1-score'] + yticklabels = ['{0} ({1})'.format(class_names[idx], sup) for idx, sup in enumerate(support)] + figure_width = 25 + figure_height = len(class_names) + 7 + correct_orientation = False + heatmap(np.array(plotMat), title, xlabel, ylabel, xticklabels, yticklabels, figure_width, figure_height, correct_orientation, cmap=cmap) + +softmax = np.load(FLAGS.softmax) +dinfo = np.load(FLAGS.dinfo) + +class_names=dinfo['classes'] + +y_true = softmax['labels'] +y_proba = softmax['predictions'] + +y_true_sparse = label_binarize(y_true, classes=np.unique(y_true)) +y_pred = np.argmax(y_proba, axis=1) + +cl_report= classification_report(y_true, y_pred, target_names=class_names, labels=np.arange(len(class_names))) +print(cl_report) + +cm = confusion_matrix(y_true, y_pred, labels=np.arange(len(class_names))) +print(cm) + +def top_wrong(cm, N=5): + a=cm + N = 150 + idx = np.argsort(a.ravel())[-N:][::-1] #single slicing: `[:N-2:-1]` + topN_val = a.ravel()[idx] + row_col = np.c_[np.unravel_index(idx, a.shape)] + return row_col + +#print(top_wrong(cm)) +for idxs in top_wrong(cm): + if idxs[0] != idxs[1]: + print(class_names[idxs[0]],"\t",class_names[idxs[1]],"\t",cm[idxs[0], idxs[1]]) + +benign_class = np.where(class_names=='benign') + +benign_pages, _ = np.where(y_true == benign_class) + +cnt=0 +cnt9=0 +for benign_page in benign_pages: + guess = y_pred[benign_page] + if guess != benign_class: + softmax_val = y_proba[benign_page][guess] + cnt +=1 + if softmax_val > 0.95: + print("B: " + class_names[guess] + "\t" + str(softmax_val)) + cnt9 += 1 + +print('We have ' + str(cnt9) + ' false-positives with softmax > 0.95 out of ' +str(cnt) + '/' + str(benign_pages.size)) diff --git a/util/plot-softmax b/util/plot-softmax new file mode 100755 index 0000000..c6c2774 --- /dev/null +++ b/util/plot-softmax @@ -0,0 +1,94 @@ +#!/usr/bin/python +import tensorflow as tf +import numpy as np +import matplotlib.pyplot as plt +import scikitplot as skplt + +from sklearn.preprocessing import label_binarize +from sklearn.preprocessing import LabelEncoder +from sklearn.metrics import auc +from sklearn.metrics import precision_recall_curve +from sklearn.metrics import average_precision_score + +flags = tf.app.flags + +flags.DEFINE_string('softmax', None, 'The softmax.npz file contained labels and probas') +flags.DEFINE_string('dinfo', None, 'The dinfo.npz file') +flags.DEFINE_integer('chunks', 4, 'The number of plots to produce') + + +FLAGS = flags.FLAGS + +softmax = np.load(FLAGS.softmax) +dinfo = np.load(FLAGS.dinfo) + +class_names=dinfo['classes'] + +y_true = softmax['labels'] +y_proba = softmax['predictions'] + + +def plot_precision_recall(y_true, y_probas, + plot_micro=True, + classes_to_plot=None, ax=None, + figsize=None, cmap='nipy_spectral', + text_fontsize="medium"): + + y_true = np.array(y_true) + y_probas = np.array(y_probas) + + classes = np.unique(y_true) + probas = y_probas + + if classes_to_plot is None: + classes_to_plot = classes + + binarized_y_true = label_binarize(y_true, classes=classes) + if len(classes) == 2: + binarized_y_true = np.hstack( + (1 - binarized_y_true, binarized_y_true)) + + fig, ax = plt.subplots(int(FLAGS.chunks/2), 2, figsize=figsize) + chunk_size = int(len(classes)/FLAGS.chunks) + int(len(classes) % FLAGS.chunks > 0) + print('Chunk size', chunk_size) + + + + indices_to_plot = np.in1d(classes, classes_to_plot) + + for i, img_class in enumerate(classes): + average_precision = average_precision_score( + binarized_y_true[:, i], + probas[:, i]) + precision, recall, _ = precision_recall_curve( + y_true, probas[:, i], pos_label=img_class) + color = plt.cm.get_cmap(cmap)(float(i%chunk_size) / chunk_size) + ax[int(i/(chunk_size*2)), int(i%(chunk_size*2) > chunk_size)].plot(recall, precision, lw=2, + label='{0} ' + '(area = {1:0.3f})'.format(class_names[int(img_class)], + average_precision), + color=color) + + if plot_micro: + precision, recall, _ = precision_recall_curve( + binarized_y_true.ravel(), probas.ravel()) + average_precision = average_precision_score(binarized_y_true, + probas, + average='micro') + ax[int(FLAGS.chunks/2)-1,1].plot(recall, precision, + label='micro-average PR ' + '(area = {0:0.3f})'.format(average_precision), + color='navy', linestyle=':', linewidth=4) + + for x in range(int(FLAGS.chunks/2)): + for y in range(2): + ax[x,y].set_xlim([0.0, 1.0]) + ax[x,y].set_ylim([0.0, 1.05]) + ax[x,y].set_xlabel('Recall') + ax[x,y].set_ylabel('Precision') + ax[x,y].tick_params(labelsize=text_fontsize) + ax[x,y].legend(loc='lower left', fontsize=text_fontsize) + return ax + +plot_precision_recall(y_true, y_proba, text_fontsize="xx-small", classes_to_plot=[3,16,41,70,77,82]) +plt.show() diff --git a/util/splitter b/util/splitter new file mode 100755 index 0000000..0373669 --- /dev/null +++ b/util/splitter @@ -0,0 +1,30 @@ +#!/usr/bin/perl +my $target; +my $md5_hash; +my $png; +my $count = 0; + + +while (<>){ + if (/(.*),,,vas,,,(.*),,,vas,,,(.*)/) { + if($target){ + mkdir "images-man/$target" unless -d "images-man/$target"; + open(my $fh, '>', "images-man/$target/$target-$md5_hash.png") or die "could not write"; + print $fh $png; + close $fh; + } + $count++; + $target = $1; + $md5_hash = $2; + $png = $3."\n"; + } else { + $png.=$_; + } +} + +mkdir "images-man/$target" unless -d "images-man/$target"; +open(my $fh, '>', "images-man/$target/$target-$md5_hash.png") or die; +print $fh $png; +close $fh; + +print($count, " images written\n"); diff --git a/util/splitter-man b/util/splitter-man new file mode 100755 index 0000000..fbdef17 --- /dev/null +++ b/util/splitter-man @@ -0,0 +1,30 @@ +#!/usr/bin/perl +my $target; +my $md5_hash; +my $png; +my $count = 0; + + +while (<>){ + if (/(.*),,,vas,,,(.*),,,vas,,,(.*)/) { + if($target){ + mkdir "images/$target" unless -d "images/$target"; + open(my $fh, '>', "images/$target/$target-$md5_hash.png") or die "could not write"; + print $fh $png; + close $fh; + } + $count++; + $target = $1; + $md5_hash = $2; + $png = $3."\n"; + } else { + $png.=$_; + } +} + +mkdir "images/$target" unless -d "images/$target"; +open(my $fh, '>', "images/$target/$target-$md5_hash.png") or die; +print $fh $png; +close $fh; + +print($count, " images written\n"); diff --git a/util/test.dump b/util/test.dump new file mode 100644 index 0000000..e69de29 -- cgit v1.2.3