From c5f21ec1e6414cca2d8814ad96e4e70e697b5997 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 13 Jun 2017 15:11:33 +0200 Subject: [PATCH 01/18] move configuration to classes --- picasso/examples/keras/config.py | 9 +-- picasso/examples/keras/model.py | 93 +++++++++++++++++++++++ picasso/examples/keras/util.py | 90 ---------------------- picasso/examples/tensorflow/config.py | 9 +-- picasso/examples/tensorflow/model.py | 52 +++++++++++++ picasso/examples/tensorflow/util.py | 48 ------------ picasso/ml_frameworks/keras/model.py | 4 +- picasso/ml_frameworks/model.py | 90 +++++----------------- picasso/ml_frameworks/tensorflow/model.py | 4 +- picasso/picasso.py | 2 + picasso/settings.py | 26 +------ tests/conftest.py | 8 +- 12 files changed, 186 insertions(+), 249 deletions(-) create mode 100644 picasso/examples/keras/model.py delete mode 100644 picasso/examples/keras/util.py create mode 100644 picasso/examples/tensorflow/model.py delete mode 100644 picasso/examples/tensorflow/util.py diff --git a/picasso/examples/keras/config.py b/picasso/examples/keras/config.py index 0ecdf76..e6423dd 100644 --- a/picasso/examples/keras/config.py +++ b/picasso/examples/keras/config.py @@ -9,11 +9,6 @@ base_dir = os.path.dirname(os.path.abspath(__file__)) -BACKEND_ML = 'keras' -BACKEND_PREPROCESSOR_NAME = 'preprocess' -BACKEND_PREPROCESSOR_PATH = os.path.join(base_dir, 'util.py') -BACKEND_POSTPROCESSOR_NAME = 'postprocess' -BACKEND_POSTPROCESSOR_PATH = os.path.join(base_dir, 'util.py') -BACKEND_PROB_DECODER_NAME = 'prob_decode' -BACKEND_PROB_DECODER_PATH = os.path.join(base_dir, 'util.py') +MODEL_CLS_PATH = os.path.join(base_dir, 'model.py') +MODEL_CLS_NAME = 'KerasMNISTModel' DATA_DIR = os.path.join(base_dir, 'data-volume') diff --git a/picasso/examples/keras/model.py b/picasso/examples/keras/model.py new file mode 100644 index 0000000..1dced40 --- /dev/null +++ b/picasso/examples/keras/model.py @@ -0,0 +1,93 @@ +from PIL import Image +from operator import itemgetter +import numpy as np + +from picasso.ml_frameworks.keras.model import KerasModel + + +MNIST_DIM = (28, 28) + + +class KerasMNISTModel(KerasModel): + + def preprocess(self, targets): + """Turn images into computation inputs + + Converts an iterable of PIL Images into a suitably-sized numpy array which + can be used as an input to the evaluation portion of the Keras/tensorflow + graph. + + Args: + targets (list of Images): a list of PIL Image objects + + Returns: + array (float32) + + """ + image_arrays = [] + for target in targets: + im = target.convert('L') + im = im.resize(MNIST_DIM, Image.ANTIALIAS) + arr = np.array(im) + image_arrays.append(arr) + + all_targets = np.array(image_arrays) + return all_targets.reshape(len(all_targets), + MNIST_DIM[0], + MNIST_DIM[1], 1).astype('float32') / 255 + + def postprocess(self, output_arr): + """Reshape arrays to original image dimensions + + Typically used for outputs or computations on intermediate layers which + make sense to represent as an image in the original dimension of the input + images (see ``SaliencyMaps``). + + Args: + output_arr (array of float32): Array of leading dimension n containing + n arrays to be reshaped + + Returns: + reshaped array + + """ + images = [] + for row in output_arr: + im_array = row.reshape(MNIST_DIM) + images.append(im_array) + + return images + + def prob_decode(self, probability_array, top=5): + """Provide class information from output probabilities + + Gives the visualization additional context for the computed class + probabilities. + + Args: + probability_array (array): class probabilities + top (int): number of class entries to return. Useful for limiting + output in models with many classes. Defaults to 5. + + Returns: + result list of dict in the format [{'index': class_index, 'name': + class_name, 'prob': class_probability}, ...] + + """ + results = [] + for row in probability_array: + entries = [] + for i, prob in enumerate(row): + entries.append({'index': i, + 'name': str(i), + 'prob': prob}) + + entries = sorted(entries, + key=itemgetter('prob'), + reverse=True)[:top] + + for entry in entries: + entry['prob'] = '{:.3f}'.format(entry['prob']) + results.append(entries) + + return results diff --git a/picasso/examples/keras/util.py b/picasso/examples/keras/util.py deleted file mode 100644 index 6f6202e..0000000 --- a/picasso/examples/keras/util.py +++ /dev/null @@ -1,90 +0,0 @@ -from PIL import Image -from operator import itemgetter -import numpy as np - -MNIST_DIM = (28, 28) - - -def preprocess(targets): - """Turn images into computation inputs - - Converts an iterable of PIL Images into a suitably-sized numpy array which - can be used as an input to the evaluation portion of the Keras/tensorflow - graph. - - Args: - targets (list of Images): a list of PIL Image objects - - Returns: - array (float32) - - """ - image_arrays = [] - for target in targets: - im = target.convert('L') - im = im.resize(MNIST_DIM, Image.ANTIALIAS) - arr = np.array(im) - image_arrays.append(arr) - - all_targets = np.array(image_arrays) - return all_targets.reshape(len(all_targets), - MNIST_DIM[0], - MNIST_DIM[1], 1).astype('float32') / 255 - - -def postprocess(output_arr): - """Reshape arrays to original image dimensions - - Typically used for outputs or computations on intermediate layers which - make sense to represent as an image in the original dimension of the input - images (see ``SaliencyMaps``). - - Args: - output_arr (array of float32): Array of leading dimension n containing - n arrays to be reshaped - - Returns: - reshaped array - - """ - images = [] - for row in output_arr: - im_array = row.reshape(MNIST_DIM) - images.append(im_array) - - return images - - -def prob_decode(probability_array, top=5): - """Provide class information from output probabilities - - Gives the visualization additional context for the computed class - probabilities. - - Args: - probability_array (array): class probabilities - top (int): number of class entries to return. Useful for limiting - output in models with many classes. Defaults to 5. - - Returns: - result list of dict in the format [{'index': class_index, 'name': - class_name, 'prob': class_probability}, ...] - - """ - results = [] - for row in probability_array: - entries = [] - for i, prob in enumerate(row): - entries.append({'index': i, - 'name': str(i), - 'prob': prob}) - - entries = sorted(entries, - key=itemgetter('prob'), - reverse=True)[:top] - - for entry in entries: - entry['prob'] = '{:.3f}'.format(entry['prob']) - results.append(entries) - - return results diff --git a/picasso/examples/tensorflow/config.py b/picasso/examples/tensorflow/config.py index 60de7b0..0a12ebb 100644 --- a/picasso/examples/tensorflow/config.py +++ b/picasso/examples/tensorflow/config.py @@ -2,13 +2,8 @@ base_dir = os.path.dirname(os.path.abspath(__file__)) -BACKEND_ML = 'tensorflow' -BACKEND_PREPROCESSOR_NAME = 'preprocess' -BACKEND_PREPROCESSOR_PATH = os.path.join(base_dir, 'util.py') -BACKEND_POSTPROCESSOR_NAME = 'postprocess' -BACKEND_POSTPROCESSOR_PATH = os.path.join(base_dir, 'util.py') -BACKEND_PROB_DECODER_NAME = 'prob_decode' -BACKEND_PROB_DECODER_PATH = os.path.join(base_dir, 'util.py') +MODEL_CLS_PATH = os.path.join(base_dir, 'model.py') +MODEL_CLS_NAME = 'TensorflowMNISTModel' BACKEND_TF_PREDICT_VAR = 'Softmax:0' BACKEND_TF_INPUT_VAR = 'convolution2d_input_1:0' DATA_DIR = os.path.join(base_dir, 'data-volume') diff --git a/picasso/examples/tensorflow/model.py b/picasso/examples/tensorflow/model.py new file mode 100644 index 0000000..0ef8ac2 --- /dev/null +++ b/picasso/examples/tensorflow/model.py @@ -0,0 +1,52 @@ +from PIL import Image +from operator import itemgetter +import numpy as np + +from picasso.ml_frameworks.tensorflow.model import TFModel + + +MNIST_DIM = (28, 28) + + +class TensorflowMNISTModel(TFModel): + def preprocess(targets): + image_arrays = [] + for target in targets: + im = target.convert('L') + im = im.resize(MNIST_DIM, Image.ANTIALIAS) + arr = np.array(im) + image_arrays.append(arr) + + all_targets = np.array(image_arrays) + return all_targets.reshape(len(all_targets), + MNIST_DIM[0], + MNIST_DIM[1], 1).astype('float32') / 255 + + + def postprocess(output_arr): + images = [] + for row in output_arr: + im_array = row.reshape(MNIST_DIM) + images.append(im_array) + + return images + + + def prob_decode(probability_array, top=5): + results = [] + for row in probability_array: + entries = [] + for i, prob in enumerate(row): + entries.append({'index': i, + 'name': str(i), + 'prob': prob}) + + entries = sorted(entries, + key=itemgetter('prob'), + reverse=True)[:top] + + for entry in entries: + entry['prob'] = '{:.3f}'.format(entry['prob']) + results.append(entries) + + return results diff --git a/picasso/examples/tensorflow/util.py b/picasso/examples/tensorflow/util.py deleted file mode 100644 index 1820184..0000000 --- a/picasso/examples/tensorflow/util.py +++ /dev/null @@ -1,48 +0,0 @@ -from PIL import Image -from operator import itemgetter -import numpy as np - -MNIST_DIM = (28, 28) - - -def preprocess(targets): - image_arrays = [] - for target in targets: - im = target.convert('L') - im = im.resize(MNIST_DIM, Image.ANTIALIAS) - arr = np.array(im) - image_arrays.append(arr) - - all_targets = np.array(image_arrays) - return all_targets.reshape(len(all_targets), - MNIST_DIM[0], - MNIST_DIM[1], 1).astype('float32') / 255 - - -def postprocess(output_arr): - images = [] - for row in output_arr: - im_array = row.reshape(MNIST_DIM) - images.append(im_array) - - return images - - -def prob_decode(probability_array, top=5): - results = [] - for row in probability_array: - entries = [] - for i, prob in enumerate(row): - entries.append({'index': i, - 'name': str(i), - 'prob': prob}) - - entries = sorted(entries, - key=itemgetter('prob'), - reverse=True)[:top] - - for entry in entries: - entry['prob'] = '{:.3f}'.format(entry['prob']) - results.append(entries) - - return results diff --git a/picasso/ml_frameworks/keras/model.py b/picasso/ml_frameworks/keras/model.py index c454264..e88aacf 100644 --- a/picasso/ml_frameworks/keras/model.py +++ b/picasso/ml_frameworks/keras/model.py @@ -6,10 +6,10 @@ import keras.backend as K from keras.models import model_from_json, load_model -from picasso.ml_frameworks.tensorflow.model import TFModel +from picasso.ml_frameworks.model import BaseModel -class KerasModel(TFModel): +class KerasModel(BaseModel): """Implements model loading functions for Keras Using this Keras module will require the h5py library, which is not included with Keras diff --git a/picasso/ml_frameworks/model.py b/picasso/ml_frameworks/model.py index 4d687b1..17c3c42 100644 --- a/picasso/ml_frameworks/model.py +++ b/picasso/ml_frameworks/model.py @@ -1,17 +1,9 @@ -import importlib.util import warnings -from importlib import import_module from operator import itemgetter +import importlib -ML_LIBRARIES = { - 'tensorflow': - 'picasso.ml_frameworks.tensorflow.model.TFModel', - 'keras': - 'picasso.ml_frameworks.keras.model.KerasModel' -} - -class Model: +class BaseModel: """Model class interface. All ML frameworks should derive from this class for the purposes of @@ -21,12 +13,6 @@ class Model: """ def __init__(self, - preprocessor_name='preprocess', - preprocessor_path=None, - postprocessor_name='postprocess', - postprocessor_path=None, - prob_decoder_name='prob_decode', - prob_decoder_path=None, top_probs=5, **kwargs): """Attempt to load utilities @@ -35,55 +21,15 @@ def __init__(self, and probability decoder if a path is supplied. Args: - preprocessor_name (str, optional): the name of the preprocessing - function. Defaults to 'preprocess'. - preprocessor_path (str, optional): the absolute path to the file - containing the function named above. If `None`, then do not - try to load a preprocessor. Defaults to `None`. - postprocessor_name (str, optional): the name of the postprocessing - function. Defaults to 'postprocess'. - postprocessor_path (str, optional): the absolute path to the file - containing the function named above. If `None`, then do not - try to load a postprocessor. Defaults to `None`. - prob_decoder_name (str, optional): the name of the postprocessing - function. Defaults to 'prob_decode'. - prob_decoder_path (str, optional): the absolute path to the file - containing the function named above. If `None`, then do not - try to load a prob_decoder. Defaults to `None`. top_probs (int): Number of classes to display per result. For instance, VGG16 has 1000 classes, we don't want to display a visualization for every single possibility. Defaults to 5. **kwargs: Arbitrary keyword arguments, useful for passing specific settings to derived classes. - Example: - If you define a function called "preprocess" at "/path/to/util.py", - then try:: - - preprocessor_name='preprocess', - preprocessor_path='/path/to/util.py' """ - self.latest_ckpt_name = None - self.latest_ckpt_time = None self.top_probs = top_probs - - self.preprocessor_name = preprocessor_name - self.preprocessor_path = preprocessor_path - self.postprocessor_name = postprocessor_name - self.postprocessor_path = postprocessor_path - self.prob_decoder_name = prob_decoder_name - self.prob_decoder_path = prob_decoder_path - - for util in ('preprocessor', 'postprocessor', 'prob_decoder'): - if getattr(self, '{}_path'.format(util)): - spec = importlib.util.\ - spec_from_file_location( - getattr(self, '{}_name'.format(util)), - getattr(self, '{}_path'.format(util))) - setattr(self, util, importlib.util.module_from_spec(spec)) - spec.loader.exec_module(getattr(self, util)) - if kwargs: for key, value in kwargs.items(): setattr(self, key, value) @@ -216,22 +162,28 @@ def decode_prob(self, output_arr): return results -def generate_model(backend_ml, **kwargs): - """Create a new instance of ML backend +def generate_model(model_cls_path, model_cls_name, **kwargs): + """Get an instance of the described model. Args: - backend_ml (:obj:`str`): name of the backend to use - **kwargs: Arbitrary keyword arguments + model_cls_path: Path to the module in which the model class + is defined. + model_cls_name: Name of the model class. + data_dir: Directory containing the graph and weights. + kwargs: Arbitrary keyword arguments passed to the model's + constructor. Returns: - An instance of :class:`.ml_frameworks.model.Model` + An instance of :class:`.ml_frameworks.model.BaseModel` or subclass """ - module_name, _, class_name = \ - ML_LIBRARIES[backend_ml].rpartition('.') - - cls = getattr(import_module(module_name), class_name) - - kwargs = {k.partition('_')[-1]: - v for (k, v) in kwargs.items()} - return cls(**kwargs) + spec = importlib.util.spec_from_file_location('active_model', + model_cls_path) + model_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(model_module) + model_cls = getattr(model_module, model_cls_name) + model = model_cls(**kwargs) + if not isinstance(model, BaseModel): + warnings.warn("Loaded model '%s' at '%s' is not an instance of %r" + % (model_cls_name, model_cls_path, BaseModel)) + return model diff --git a/picasso/ml_frameworks/tensorflow/model.py b/picasso/ml_frameworks/tensorflow/model.py index 97a1c3d..a8b4813 100644 --- a/picasso/ml_frameworks/tensorflow/model.py +++ b/picasso/ml_frameworks/tensorflow/model.py @@ -4,10 +4,10 @@ import tensorflow as tf -from picasso.ml_frameworks.model import Model +from picasso.ml_frameworks.model import BaseModel -class TFModel(Model): +class TFModel(BaseModel): """Implements model loading functions for tensorflow""" def load(self, data_dir='./'): diff --git a/picasso/picasso.py b/picasso/picasso.py index 10d141a..4b341dd 100644 --- a/picasso/picasso.py +++ b/picasso/picasso.py @@ -78,6 +78,8 @@ # machine. ml_backend = \ generate_model( + app.config['MODEL_CLS_PATH'], + app.config['MODEL_CLS_NAME'], **{k.lower(): v for (k, v) in app.config.items() if k.startswith('BACKEND')} diff --git a/picasso/settings.py b/picasso/settings.py index d469651..bc9c3c8 100644 --- a/picasso/settings.py +++ b/picasso/settings.py @@ -15,29 +15,11 @@ class Default: the source. """ - #: :obj:`str`: which backend to use - BACKEND_ML = 'keras' + #: :obj:`str`: filepath of the module containing the model to run + MODEL_CLS_PATH = os.path.join(base_dir, 'examples', 'keras', 'model.py') - #: :obj:`str`: name of the preprocess function - BACKEND_PREPROCESSOR_NAME = 'preprocess' - - #: :obj:`str`: filepath of the preprocess function - BACKEND_PREPROCESSOR_PATH = os.path.join( - base_dir, 'examples', 'keras', 'util.py') - - #: :obj:`str`: name of the postprocess function - BACKEND_POSTPROCESSOR_NAME = 'postprocess' - - #: :obj:`str`: filepath of the postprocess function - BACKEND_POSTPROCESSOR_PATH = os.path.join( - base_dir, 'examples', 'keras', 'util.py') - - #: :obj:`str`: name of the probability decoder function - BACKEND_PROB_DECODER_NAME = 'prob_decode' - - #: :obj:`str`: filepath of the probability decoder function - BACKEND_PROB_DECODER_PATH = os.path.join( - base_dir, 'examples', 'keras', 'util.py') + #: :obj:`str`: name of model class + MODEL_CLS_NAME = 'KerasMNISTModel' #: :obj:`str`: path to directory containing weights and graph DATA_DIR = os.path.join( diff --git a/tests/conftest.py b/tests/conftest.py index 777ac6f..ee74278 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,5 +27,9 @@ def example_prob_array(): @pytest.fixture def base_model(): - from picasso.ml_frameworks.model import Model - return Model() + from picasso.ml_frameworks.model import BaseModel + + class BaseModelForTest(BaseModel): + def load(self, data_dir): + pass + return BaseModelForTest() From 0a58babce835e0e28c208cfb5ca910625ed56bf1 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 13 Jun 2017 15:22:33 +0200 Subject: [PATCH 02/18] move configuration to classes --- picasso/examples/keras-vgg16/config.py | 9 +---- picasso/examples/keras-vgg16/model.py | 56 ++++++++++++++++++++++++++ picasso/examples/keras-vgg16/util.py | 53 ------------------------ picasso/examples/tensorflow/model.py | 9 ++--- 4 files changed, 62 insertions(+), 65 deletions(-) create mode 100644 picasso/examples/keras-vgg16/model.py delete mode 100644 picasso/examples/keras-vgg16/util.py diff --git a/picasso/examples/keras-vgg16/config.py b/picasso/examples/keras-vgg16/config.py index 7ab3fa5..da0fedf 100644 --- a/picasso/examples/keras-vgg16/config.py +++ b/picasso/examples/keras-vgg16/config.py @@ -2,11 +2,6 @@ base_dir = os.path.dirname(os.path.abspath(__file__)) -BACKEND_ML = 'keras' -BACKEND_PREPROCESSOR_NAME = 'preprocess' -BACKEND_PREPROCESSOR_PATH = os.path.join(base_dir, 'util.py') -BACKEND_POSTPROCESSOR_NAME = 'postprocess' -BACKEND_POSTPROCESSOR_PATH = os.path.join(base_dir, 'util.py') -BACKEND_PROB_DECODER_NAME = 'prob_decode' -BACKEND_PROB_DECODER_PATH = os.path.join(base_dir, 'util.py') +MODEL_CLS_PATH = os.path.join(base_dir, 'model.py') +MODEL_CLS_NAME = 'KerasVGG16Model' DATA_DIR = os.path.join(base_dir, 'data-volume') diff --git a/picasso/examples/keras-vgg16/model.py b/picasso/examples/keras-vgg16/model.py new file mode 100644 index 0000000..608ec64 --- /dev/null +++ b/picasso/examples/keras-vgg16/model.py @@ -0,0 +1,56 @@ +from keras.applications.imagenet_utils import (decode_predictions, + preprocess_input) +import keras.applications.imagenet_utils +from PIL import Image +import numpy as np + +from picasso.ml_frameworks.keras.model import KerasModel + + +VGG16_DIM = (224, 224, 3) + + +class KerasVGG16Model(KerasModel): + + def preprocess(self, targets): + image_arrays = [] + for target in targets: + im = target.resize(VGG16_DIM[:2], Image.ANTIALIAS) + im = im.convert('RGB') + arr = np.array(im).astype('float32') + image_arrays.append(arr) + + all_targets = np.array(image_arrays) + return preprocess_input(all_targets) + + def postprocess(self, output_arr): + images = [] + for row in output_arr: + im_array = row.reshape(VGG16_DIM[:2]) + images.append(im_array) + + return images + + def prob_decode(self, probability_array, top=5): + r = decode_predictions(probability_array, top=top) + results = [ + [{'code': entry[0], + 'name': entry[1], + 'prob': '{:.3f}'.format(entry[2])} + for entry in row] + for row in r + ] + classes = keras.applications.imagenet_utils.CLASS_INDEX + class_keys = list(classes.keys()) + class_values = list(classes.values()) + + for result in results: + for entry in result: + entry.update( + {'index': + int( + class_keys[class_values.index([entry['code'], + entry['name']])] + )} + ) + return results diff --git a/picasso/examples/keras-vgg16/util.py b/picasso/examples/keras-vgg16/util.py deleted file mode 100644 index e56eaa0..0000000 --- a/picasso/examples/keras-vgg16/util.py +++ /dev/null @@ -1,53 +0,0 @@ -from keras.applications.imagenet_utils import (decode_predictions, - preprocess_input) -import keras.applications.imagenet_utils -from PIL import Image -import numpy as np - -VGG16_DIM = (224, 224, 3) - - -def preprocess(targets): - image_arrays = [] - for target in targets: - im = target.resize(VGG16_DIM[:2], Image.ANTIALIAS) - im = im.convert('RGB') - arr = np.array(im).astype('float32') - image_arrays.append(arr) - - all_targets = np.array(image_arrays) - return preprocess_input(all_targets) - - -def postprocess(output_arr): - images = [] - for row in output_arr: - im_array = row.reshape(VGG16_DIM[:2]) - images.append(im_array) - - return images - - -def prob_decode(probability_array, top=5): - r = decode_predictions(probability_array, top=top) - results = [ - [{'code': entry[0], - 'name': entry[1], - 'prob': '{:.3f}'.format(entry[2])} - for entry in row] - for row in r - ] - classes = keras.applications.imagenet_utils.CLASS_INDEX - class_keys = list(classes.keys()) - class_values = list(classes.values()) - - for result in results: - for entry in result: - entry.update( - {'index': - int( - class_keys[class_values.index([entry['code'], - entry['name']])] - )} - ) - return results diff --git a/picasso/examples/tensorflow/model.py b/picasso/examples/tensorflow/model.py index 0ef8ac2..b80e5bf 100644 --- a/picasso/examples/tensorflow/model.py +++ b/picasso/examples/tensorflow/model.py @@ -9,7 +9,8 @@ class TensorflowMNISTModel(TFModel): - def preprocess(targets): + + def preprocess(self, targets): image_arrays = [] for target in targets: im = target.convert('L') @@ -22,8 +23,7 @@ def preprocess(targets): MNIST_DIM[0], MNIST_DIM[1], 1).astype('float32') / 255 - - def postprocess(output_arr): + def postprocess(self, output_arr): images = [] for row in output_arr: im_array = row.reshape(MNIST_DIM) @@ -31,8 +31,7 @@ def postprocess(output_arr): return images - - def prob_decode(probability_array, top=5): + def prob_decode(self, probability_array, top=5): results = [] for row in probability_array: entries = [] From def486dbb49210dc29c635fd71ce365aa4796ad3 Mon Sep 17 00:00:00 2001 From: Josh Chen Date: Tue, 13 Jun 2017 16:23:09 +0200 Subject: [PATCH 03/18] get branch running --- picasso/examples/tensorflow/model.py | 4 ++++ picasso/ml_frameworks/model.py | 2 ++ picasso/ml_frameworks/tensorflow/model.py | 12 ++++++++++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/picasso/examples/tensorflow/model.py b/picasso/examples/tensorflow/model.py index b80e5bf..47f57fa 100644 --- a/picasso/examples/tensorflow/model.py +++ b/picasso/examples/tensorflow/model.py @@ -10,6 +10,10 @@ class TensorflowMNISTModel(TFModel): + TF_INPUT_VAR = 'convolution2d_input_1:0' + + TF_PREDICT_VAR = 'Softmax:0' + def preprocess(self, targets): image_arrays = [] for target in targets: diff --git a/picasso/ml_frameworks/model.py b/picasso/ml_frameworks/model.py index 17c3c42..d876655 100644 --- a/picasso/ml_frameworks/model.py +++ b/picasso/ml_frameworks/model.py @@ -29,6 +29,8 @@ def __init__(self, """ + self.latest_ckpt_name = None + self.latest_ckpt_time = None self.top_probs = top_probs if kwargs: for key, value in kwargs.items(): diff --git a/picasso/ml_frameworks/tensorflow/model.py b/picasso/ml_frameworks/tensorflow/model.py index a8b4813..cc36481 100644 --- a/picasso/ml_frameworks/tensorflow/model.py +++ b/picasso/ml_frameworks/tensorflow/model.py @@ -10,6 +10,14 @@ class TFModel(BaseModel): """Implements model loading functions for tensorflow""" + # Name of the tensor corresponding to the model's inputs. You must define + # this if you are loading the model from a checkpoint. + TF_INPUT_VAR = None + + # Name of the tensor corresponding to the model's inputs. You must define + # this if you are loading the model from a checkpoint. + TF_PREDICT_VAR = None + def load(self, data_dir='./'): """Load graph and weight data @@ -61,9 +69,9 @@ def load(self, data_dir='./'): self.saver.restore(sess, latest_ckpt) self.tf_predict_var = \ - self.sess.graph.get_tensor_by_name(self.tf_predict_var) + self.sess.graph.get_tensor_by_name(self.TF_PREDICT_VAR) self.tf_input_var = \ - self.sess.graph.get_tensor_by_name(self.tf_input_var) + self.sess.graph.get_tensor_by_name(self.TF_INPUT_VAR) def _predict(self, input_array): return self.sess.run(self.tf_predict_var, From 40e311c18fad494fb5200eaebfcb38c0992e0690 Mon Sep 17 00:00:00 2001 From: Josh Chen Date: Tue, 13 Jun 2017 16:59:31 +0200 Subject: [PATCH 04/18] get test passing --- tests/test_picasso.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/test_picasso.py b/tests/test_picasso.py index 95ec38f..bf009e6 100644 --- a/tests/test_picasso.py +++ b/tests/test_picasso.py @@ -96,18 +96,21 @@ def test_saved_model(self): assert km.tf_predict_var is not None -class TestTensorflowBackend: +from picasso.ml_frameworks.tensorflow.model import TFModel +class TFTestModel(TFModel): + TF_PREDICT_VAR = 'Softmax:0' + TF_INPUT_VAR = 'convolution2d_input_1:0' - def test_tensorflow_backend(self, client, monkeypatch): + +class TestTensorflowModel: + + def test_tensorflow_model(self, client, monkeypatch): """Only tests tensorflow backend loads without error """ - - from picasso.ml_frameworks.tensorflow.model import TFModel data_path = os.path.join('picasso', 'examples', 'tensorflow', 'data-volume') - tfm = TFModel(tf_predict_var='Softmax:0', - tf_input_var='convolution2d_input_1:0') + tfm = TFTestModel() tfm.load(data_path) assert tfm.tf_predict_var is not None assert tfm.tf_input_var is not None From b1d281ea8b64a9b348d2a5a482461ed9b65e825f Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 14 Jun 2017 11:25:37 +0200 Subject: [PATCH 05/18] fixup model files: docstring length, function scope --- picasso/examples/keras-vgg16/model.py | 9 ++++++--- picasso/examples/keras/model.py | 25 ++++++++++++++----------- picasso/examples/tensorflow/model.py | 9 ++++++--- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/picasso/examples/keras-vgg16/model.py b/picasso/examples/keras-vgg16/model.py index 608ec64..807fdfd 100644 --- a/picasso/examples/keras-vgg16/model.py +++ b/picasso/examples/keras-vgg16/model.py @@ -12,7 +12,8 @@ class KerasVGG16Model(KerasModel): - def preprocess(self, targets): + @staticmethod + def preprocess(targets): image_arrays = [] for target in targets: im = target.resize(VGG16_DIM[:2], Image.ANTIALIAS) @@ -23,7 +24,8 @@ def preprocess(self, targets): all_targets = np.array(image_arrays) return preprocess_input(all_targets) - def postprocess(self, output_arr): + @staticmethod + def postprocess(output_arr): images = [] for row in output_arr: im_array = row.reshape(VGG16_DIM[:2]) @@ -31,7 +33,8 @@ def postprocess(self, output_arr): return images - def prob_decode(self, probability_array, top=5): + @staticmethod + def prob_decode(probability_array, top=5): r = decode_predictions(probability_array, top=top) results = [ [{'code': entry[0], diff --git a/picasso/examples/keras/model.py b/picasso/examples/keras/model.py index 1dced40..8413d79 100644 --- a/picasso/examples/keras/model.py +++ b/picasso/examples/keras/model.py @@ -10,12 +10,13 @@ class KerasMNISTModel(KerasModel): - def preprocess(self, targets): + @staticmethod + def preprocess(targets): """Turn images into computation inputs - Converts an iterable of PIL Images into a suitably-sized numpy array which - can be used as an input to the evaluation portion of the Keras/tensorflow - graph. + Converts an iterable of PIL Images into a suitably-sized numpy + array which can be used as an input to the evaluation portion + of the Keras/tensorflow graph. Args: targets (list of Images): a list of PIL Image objects @@ -36,16 +37,17 @@ def preprocess(self, targets): MNIST_DIM[0], MNIST_DIM[1], 1).astype('float32') / 255 - def postprocess(self, output_arr): + @staticmethod + def postprocess(output_arr): """Reshape arrays to original image dimensions - Typically used for outputs or computations on intermediate layers which - make sense to represent as an image in the original dimension of the input - images (see ``SaliencyMaps``). + Typically used for outputs or computations on intermediate + layers which make sense to represent as an image in the original + dimension of the input images (see ``SaliencyMaps``). Args: - output_arr (array of float32): Array of leading dimension n containing - n arrays to be reshaped + output_arr (array of float32): Array of leading dimension n + containing n arrays to be reshaped Returns: reshaped array @@ -58,7 +60,8 @@ def postprocess(self, output_arr): return images - def prob_decode(self, probability_array, top=5): + @staticmethod + def prob_decode(probability_array, top=5): """Provide class information from output probabilities Gives the visualization additional context for the computed class diff --git a/picasso/examples/tensorflow/model.py b/picasso/examples/tensorflow/model.py index 47f57fa..6c10ea6 100644 --- a/picasso/examples/tensorflow/model.py +++ b/picasso/examples/tensorflow/model.py @@ -14,7 +14,8 @@ class TensorflowMNISTModel(TFModel): TF_PREDICT_VAR = 'Softmax:0' - def preprocess(self, targets): + @staticmethod + def preprocess(targets): image_arrays = [] for target in targets: im = target.convert('L') @@ -27,7 +28,8 @@ def preprocess(self, targets): MNIST_DIM[0], MNIST_DIM[1], 1).astype('float32') / 255 - def postprocess(self, output_arr): + @staticmethod + def postprocess(output_arr): images = [] for row in output_arr: im_array = row.reshape(MNIST_DIM) @@ -35,7 +37,8 @@ def postprocess(self, output_arr): return images - def prob_decode(self, probability_array, top=5): + @staticmethod + def prob_decode(probability_array, top=5): results = [] for row in probability_array: entries = [] From d90fe0cdae5c1ea387ff4eba63e62bb3609ec6d5 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 14 Jun 2017 11:35:15 +0200 Subject: [PATCH 06/18] fix config --- picasso/picasso.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/picasso/picasso.py b/picasso/picasso.py index 4b341dd..87ab330 100644 --- a/picasso/picasso.py +++ b/picasso/picasso.py @@ -80,7 +80,9 @@ generate_model( app.config['MODEL_CLS_PATH'], app.config['MODEL_CLS_NAME'], - **{k.lower(): v for (k, v) + # passes along all settings prefixed with + # "BACKEND_" without the prefix + **{k[8:]: v for (k, v) in app.config.items() if k.startswith('BACKEND')} ) From ec77da8eb81017bcf4f269c4dac97525f5a9077b Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 14 Jun 2017 11:41:20 +0200 Subject: [PATCH 07/18] consistent attributes --- picasso/ml_frameworks/keras/model.py | 8 ++++---- tests/test_picasso.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/picasso/ml_frameworks/keras/model.py b/picasso/ml_frameworks/keras/model.py index e88aacf..a26c1d3 100644 --- a/picasso/ml_frameworks/keras/model.py +++ b/picasso/ml_frameworks/keras/model.py @@ -16,9 +16,9 @@ class KerasModel(BaseModel): Attributes: sess (Tensorflow :obj:`Session`): underlying Tensorflow session of the Keras model. - tf_predict_var (:obj:`Tensor`): tensorflow tensor which represents + TF_PREDICT_VAR (:obj:`Tensor`): tensorflow tensor which represents the class probabilities - tf_input_var (:obj:`Tensor`): tensorflow tensor which represents + TF_INPUT_VAR (:obj:`Tensor`): tensorflow tensor which represents the inputs """ @@ -65,8 +65,8 @@ def load(self, data_dir='./'): self.sess = K.get_session() - self.tf_predict_var = self.model.outputs[0] - self.tf_input_var = self.model.inputs[0] + self.TF_PREDICT_VAR = self.model.outputs[0] + self.TF_INPUT_VAR = self.model.inputs[0] def _predict(self, input_array): return self.model.predict(input_array) diff --git a/tests/test_picasso.py b/tests/test_picasso.py index bf009e6..a2a5acc 100644 --- a/tests/test_picasso.py +++ b/tests/test_picasso.py @@ -93,7 +93,7 @@ def test_saved_model(self): km = KerasModel() km.load(temp) - assert km.tf_predict_var is not None + assert km.TF_PREDICT_VAR is not None from picasso.ml_frameworks.tensorflow.model import TFModel From d7a9c2a851239018d1110b4ad995a0ba9917e684 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 14 Jun 2017 11:44:32 +0200 Subject: [PATCH 08/18] don't override settings in model example --- picasso/examples/tensorflow/model.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/picasso/examples/tensorflow/model.py b/picasso/examples/tensorflow/model.py index 6c10ea6..f044411 100644 --- a/picasso/examples/tensorflow/model.py +++ b/picasso/examples/tensorflow/model.py @@ -10,10 +10,6 @@ class TensorflowMNISTModel(TFModel): - TF_INPUT_VAR = 'convolution2d_input_1:0' - - TF_PREDICT_VAR = 'Softmax:0' - @staticmethod def preprocess(targets): image_arrays = [] From 371a43b2d9e01a21efea27a35cba4bdc7b1ee54d Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 14 Jun 2017 11:58:44 +0200 Subject: [PATCH 09/18] parameterize tests --- tests/conftest.py | 10 ++++++++++ tests/test_picasso.py | 15 ++++----------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index ee74278..da4ee8a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,3 +33,13 @@ class BaseModelForTest(BaseModel): def load(self, data_dir): pass return BaseModelForTest() + + +@pytest.fixture +def tensorflow_model(): + from picasso.ml_frameworks.tensorflow.model import TFModel + + tfm = TFModel(TF_PREDICT_VAR='Softmax:0', + TF_INPUT_VAR='convolution2d_input_1:0') + + return tfm diff --git a/tests/test_picasso.py b/tests/test_picasso.py index a2a5acc..d2dbbca 100644 --- a/tests/test_picasso.py +++ b/tests/test_picasso.py @@ -96,21 +96,14 @@ def test_saved_model(self): assert km.TF_PREDICT_VAR is not None -from picasso.ml_frameworks.tensorflow.model import TFModel -class TFTestModel(TFModel): - TF_PREDICT_VAR = 'Softmax:0' - TF_INPUT_VAR = 'convolution2d_input_1:0' - - class TestTensorflowModel: - def test_tensorflow_model(self, client, monkeypatch): + def test_tensorflow_model(self, client, tensorflow_model): """Only tests tensorflow backend loads without error """ data_path = os.path.join('picasso', 'examples', 'tensorflow', 'data-volume') - tfm = TFTestModel() - tfm.load(data_path) - assert tfm.tf_predict_var is not None - assert tfm.tf_input_var is not None + tensorflow_model.load(data_path) + assert tensorflow_model.TF_PREDICT_VAR is not None + assert tensorflow_model.TF_INPUT_VAR is not None From 91b8df8693580af14dc59bf3eb836889d2bceb8b Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 14 Jun 2017 12:09:26 +0200 Subject: [PATCH 10/18] revert to lowercase attributes --- picasso/ml_frameworks/keras/model.py | 8 ++++---- picasso/ml_frameworks/tensorflow/model.py | 8 ++++---- picasso/picasso.py | 4 ++-- tests/conftest.py | 4 ++-- tests/test_picasso.py | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/picasso/ml_frameworks/keras/model.py b/picasso/ml_frameworks/keras/model.py index a26c1d3..e88aacf 100644 --- a/picasso/ml_frameworks/keras/model.py +++ b/picasso/ml_frameworks/keras/model.py @@ -16,9 +16,9 @@ class KerasModel(BaseModel): Attributes: sess (Tensorflow :obj:`Session`): underlying Tensorflow session of the Keras model. - TF_PREDICT_VAR (:obj:`Tensor`): tensorflow tensor which represents + tf_predict_var (:obj:`Tensor`): tensorflow tensor which represents the class probabilities - TF_INPUT_VAR (:obj:`Tensor`): tensorflow tensor which represents + tf_input_var (:obj:`Tensor`): tensorflow tensor which represents the inputs """ @@ -65,8 +65,8 @@ def load(self, data_dir='./'): self.sess = K.get_session() - self.TF_PREDICT_VAR = self.model.outputs[0] - self.TF_INPUT_VAR = self.model.inputs[0] + self.tf_predict_var = self.model.outputs[0] + self.tf_input_var = self.model.inputs[0] def _predict(self, input_array): return self.model.predict(input_array) diff --git a/picasso/ml_frameworks/tensorflow/model.py b/picasso/ml_frameworks/tensorflow/model.py index cc36481..2689808 100644 --- a/picasso/ml_frameworks/tensorflow/model.py +++ b/picasso/ml_frameworks/tensorflow/model.py @@ -12,11 +12,11 @@ class TFModel(BaseModel): # Name of the tensor corresponding to the model's inputs. You must define # this if you are loading the model from a checkpoint. - TF_INPUT_VAR = None + tf_input_var = None # Name of the tensor corresponding to the model's inputs. You must define # this if you are loading the model from a checkpoint. - TF_PREDICT_VAR = None + tf_predict_var = None def load(self, data_dir='./'): """Load graph and weight data @@ -69,9 +69,9 @@ def load(self, data_dir='./'): self.saver.restore(sess, latest_ckpt) self.tf_predict_var = \ - self.sess.graph.get_tensor_by_name(self.TF_PREDICT_VAR) + self.sess.graph.get_tensor_by_name(self.tf_predict_var) self.tf_input_var = \ - self.sess.graph.get_tensor_by_name(self.TF_INPUT_VAR) + self.sess.graph.get_tensor_by_name(self.tf_input_var) def _predict(self, input_array): return self.sess.run(self.tf_predict_var, diff --git a/picasso/picasso.py b/picasso/picasso.py index 87ab330..20314a6 100644 --- a/picasso/picasso.py +++ b/picasso/picasso.py @@ -81,8 +81,8 @@ app.config['MODEL_CLS_PATH'], app.config['MODEL_CLS_NAME'], # passes along all settings prefixed with - # "BACKEND_" without the prefix - **{k[8:]: v for (k, v) + # "BACKEND_" without the prefix, as lowercase + **{k.lower()[8:]: v for (k, v) in app.config.items() if k.startswith('BACKEND')} ) diff --git a/tests/conftest.py b/tests/conftest.py index da4ee8a..c7d66f9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,7 +39,7 @@ def load(self, data_dir): def tensorflow_model(): from picasso.ml_frameworks.tensorflow.model import TFModel - tfm = TFModel(TF_PREDICT_VAR='Softmax:0', - TF_INPUT_VAR='convolution2d_input_1:0') + tfm = TFModel(tf_predict_var='Softmax:0', + tf_input_var='convolution2d_input_1:0') return tfm diff --git a/tests/test_picasso.py b/tests/test_picasso.py index d2dbbca..496fb70 100644 --- a/tests/test_picasso.py +++ b/tests/test_picasso.py @@ -93,7 +93,7 @@ def test_saved_model(self): km = KerasModel() km.load(temp) - assert km.TF_PREDICT_VAR is not None + assert km.tf_predict_var is not None class TestTensorflowModel: @@ -105,5 +105,5 @@ def test_tensorflow_model(self, client, tensorflow_model): data_path = os.path.join('picasso', 'examples', 'tensorflow', 'data-volume') tensorflow_model.load(data_path) - assert tensorflow_model.TF_PREDICT_VAR is not None - assert tensorflow_model.TF_INPUT_VAR is not None + assert tensorflow_model.tf_predict_var is not None + assert tensorflow_model.tf_input_var is not None From a2337b682521af684919a952c818937a2d7b8555 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 14 Jun 2017 12:33:27 +0200 Subject: [PATCH 11/18] add deprecation error --- picasso/__init__.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/picasso/__init__.py b/picasso/__init__.py index d931652..f1cb90b 100644 --- a/picasso/__init__.py +++ b/picasso/__init__.py @@ -18,4 +18,18 @@ if os.getenv('PICASSO_SETTINGS'): app.config.from_envvar('PICASSO_SETTINGS') +deprecated_settings = ['BACKEND_PREPROCESSOR_NAME', + 'BACKEND_PREPROCESSOR_PATH', + 'BACKEND_POSTPROCESSOR_NAME', + 'BACKEND_POSTPROCESSOR_PATH', + 'BACKEND_PROB_DECODER_NAME', + 'BACKEND_PROB_DECODER_PATH'] + +if any([x in app.config.keys() for x in deprecated_settings]): + raise ValueError('It looks like you\'re using a deprecated' + ' setting. The settings and utility functions' + ' have been changed as of version v0.2.0 (and ' + 'you\'re using {}). Changing to the updated ' + ' settings is trivial: see xxxxxxx.'.format(__version__)) + import picasso.picasso From c2a8b5805f3075f73a7d50a0b05a3ae29ea7e716 Mon Sep 17 00:00:00 2001 From: Josh Chen Date: Wed, 5 Jul 2017 11:37:11 +0200 Subject: [PATCH 12/18] Code style and formatting changes. --- picasso/examples/keras-vgg16/config.py | 10 +- picasso/examples/keras-vgg16/model.py | 43 ++- picasso/examples/keras/config.py | 13 +- picasso/examples/keras/model.py | 88 +----- picasso/examples/tensorflow/config.py | 14 +- picasso/examples/tensorflow/model.py | 60 ++-- picasso/ml_frameworks/keras/model.py | 55 ++-- picasso/ml_frameworks/model.py | 258 +++++++++--------- picasso/ml_frameworks/tensorflow/model.py | 69 +++-- picasso/picasso.py | 74 ++--- picasso/settings.py | 32 ++- picasso/templates/layout.html | 22 +- picasso/visualizations/__init__.py | 56 +++- picasso/visualizations/class_probabilities.py | 5 +- picasso/visualizations/partial_occlusion.py | 47 ++-- picasso/visualizations/saliency_maps.py | 48 ++-- tests/conftest.py | 7 +- tests/test_picasso.py | 12 +- 18 files changed, 424 insertions(+), 489 deletions(-) diff --git a/picasso/examples/keras-vgg16/config.py b/picasso/examples/keras-vgg16/config.py index da0fedf..4cbde9a 100644 --- a/picasso/examples/keras-vgg16/config.py +++ b/picasso/examples/keras-vgg16/config.py @@ -1,7 +1,15 @@ +# Note: By default, Flask doesn't know that this file exists. If you want +# Flask to load the settings you specify here, you must set the environment +# variable `PICASSO_SETTINGS` to point to this file. E.g.: +# +# export PICASSO_SETTINGS=/path/to/examples/keras-vgg16/config.py +# import os base_dir = os.path.dirname(os.path.abspath(__file__)) MODEL_CLS_PATH = os.path.join(base_dir, 'model.py') MODEL_CLS_NAME = 'KerasVGG16Model' -DATA_DIR = os.path.join(base_dir, 'data-volume') +MODEL_LOAD_ARGS = { + 'data_dir': os.path.join(base_dir, 'data-volume'), +} diff --git a/picasso/examples/keras-vgg16/model.py b/picasso/examples/keras-vgg16/model.py index 807fdfd..10e0658 100644 --- a/picasso/examples/keras-vgg16/model.py +++ b/picasso/examples/keras-vgg16/model.py @@ -1,8 +1,8 @@ from keras.applications.imagenet_utils import (decode_predictions, preprocess_input) import keras.applications.imagenet_utils -from PIL import Image import numpy as np +from PIL import Image from picasso.ml_frameworks.keras.model import KerasModel @@ -12,30 +12,25 @@ class KerasVGG16Model(KerasModel): - @staticmethod - def preprocess(targets): + def preprocess(self, raw_inputs): + """ + Args: + raw_inputs (list of Images): a list of PIL Image objects + Returns: + array (float32): num images * height * width * num channels + """ image_arrays = [] - for target in targets: - im = target.resize(VGG16_DIM[:2], Image.ANTIALIAS) + for raw_im in raw_inputs: + im = raw_im.resize(VGG16_DIM[:2], Image.ANTIALIAS) im = im.convert('RGB') arr = np.array(im).astype('float32') image_arrays.append(arr) - all_targets = np.array(image_arrays) - return preprocess_input(all_targets) - - @staticmethod - def postprocess(output_arr): - images = [] - for row in output_arr: - im_array = row.reshape(VGG16_DIM[:2]) - images.append(im_array) - - return images + all_raw_inputs = np.array(image_arrays) + return preprocess_input(all_raw_inputs) - @staticmethod - def prob_decode(probability_array, top=5): - r = decode_predictions(probability_array, top=top) + def decode_prob(self, class_probabilities): + r = decode_predictions(class_probabilities, top=self.top_probs) results = [ [{'code': entry[0], 'name': entry[1], @@ -49,11 +44,7 @@ def prob_decode(probability_array, top=5): for result in results: for entry in result: - entry.update( - {'index': - int( - class_keys[class_values.index([entry['code'], - entry['name']])] - )} - ) + entry['index'] = int( + class_keys[class_values.index([entry['code'], + entry['name']])]) return results diff --git a/picasso/examples/keras/config.py b/picasso/examples/keras/config.py index e6423dd..994ce70 100644 --- a/picasso/examples/keras/config.py +++ b/picasso/examples/keras/config.py @@ -1,14 +1,15 @@ -# Note: this settings file duplicates the default settings in the top-level -# file `settings.py`. If you want to modify settings here, you must export the -# path to this file: +# Note: By default, Flask doesn't know that this file exists. If you want +# Flask to load the settings you specify here, you must set the environment +# variable `PICASSO_SETTINGS` to point to this file. E.g.: # -# export PICASSO_SETTINGS=/path/to/picasso/picasso/examples/keras/config.py +# export PICASSO_SETTINGS=/path/to/examples/keras/config.py # -# otherwise, these settings will not be loaded. import os base_dir = os.path.dirname(os.path.abspath(__file__)) MODEL_CLS_PATH = os.path.join(base_dir, 'model.py') MODEL_CLS_NAME = 'KerasMNISTModel' -DATA_DIR = os.path.join(base_dir, 'data-volume') +MODEL_LOAD_ARGS = { + 'data_dir': os.path.join(base_dir, 'data-volume'), +} diff --git a/picasso/examples/keras/model.py b/picasso/examples/keras/model.py index 8413d79..adbe03d 100644 --- a/picasso/examples/keras/model.py +++ b/picasso/examples/keras/model.py @@ -1,6 +1,5 @@ -from PIL import Image -from operator import itemgetter import numpy as np +from PIL import Image from picasso.ml_frameworks.keras.model import KerasModel @@ -10,87 +9,28 @@ class KerasMNISTModel(KerasModel): - @staticmethod - def preprocess(targets): - """Turn images into computation inputs + def preprocess(self, raw_inputs): + """Convert images into the format required by our model. - Converts an iterable of PIL Images into a suitably-sized numpy - array which can be used as an input to the evaluation portion - of the Keras/tensorflow graph. + Our model requires that inputs be grayscale (mode 'L'), be resized to + `MNIST_DIM`, and be represented as float32 numpy arrays in range + [0, 1]. Args: - targets (list of Images): a list of PIL Image objects + raw_inputs (list of Images): a list of PIL Image objects Returns: - array (float32) + array (float32): num images * height * width * num channels """ image_arrays = [] - for target in targets: - im = target.convert('L') + for raw_im in raw_inputs: + im = raw_im.convert('L') im = im.resize(MNIST_DIM, Image.ANTIALIAS) arr = np.array(im) image_arrays.append(arr) - all_targets = np.array(image_arrays) - return all_targets.reshape(len(all_targets), - MNIST_DIM[0], - MNIST_DIM[1], 1).astype('float32') / 255 - - @staticmethod - def postprocess(output_arr): - """Reshape arrays to original image dimensions - - Typically used for outputs or computations on intermediate - layers which make sense to represent as an image in the original - dimension of the input images (see ``SaliencyMaps``). - - Args: - output_arr (array of float32): Array of leading dimension n - containing n arrays to be reshaped - - Returns: - reshaped array - - """ - images = [] - for row in output_arr: - im_array = row.reshape(MNIST_DIM) - images.append(im_array) - - return images - - @staticmethod - def prob_decode(probability_array, top=5): - """Provide class information from output probabilities - - Gives the visualization additional context for the computed class - probabilities. - - Args: - probability_array (array): class probabilities - top (int): number of class entries to return. Useful for limiting - output in models with many classes. Defaults to 5. - - Returns: - result list of dict in the format [{'index': class_index, 'name': - class_name, 'prob': class_probability}, ...] - - """ - results = [] - for row in probability_array: - entries = [] - for i, prob in enumerate(row): - entries.append({'index': i, - 'name': str(i), - 'prob': prob}) - - entries = sorted(entries, - key=itemgetter('prob'), - reverse=True)[:top] - - for entry in entries: - entry['prob'] = '{:.3f}'.format(entry['prob']) - results.append(entries) - - return results + inputs = np.array(image_arrays) + return inputs.reshape(len(inputs), + MNIST_DIM[0], + MNIST_DIM[1], 1).astype('float32') / 255 diff --git a/picasso/examples/tensorflow/config.py b/picasso/examples/tensorflow/config.py index 0a12ebb..08e8e11 100644 --- a/picasso/examples/tensorflow/config.py +++ b/picasso/examples/tensorflow/config.py @@ -1,9 +1,17 @@ +# Note: By default, Flask doesn't know that this file exists. If you want +# Flask to load the settings you specify here, you must set the environment +# variable `PICASSO_SETTINGS` to point to this file. E.g.: +# +# export PICASSO_SETTINGS=/path/to/examples/tensorflow/config.py +# import os base_dir = os.path.dirname(os.path.abspath(__file__)) MODEL_CLS_PATH = os.path.join(base_dir, 'model.py') MODEL_CLS_NAME = 'TensorflowMNISTModel' -BACKEND_TF_PREDICT_VAR = 'Softmax:0' -BACKEND_TF_INPUT_VAR = 'convolution2d_input_1:0' -DATA_DIR = os.path.join(base_dir, 'data-volume') +MODEL_LOAD_ARGS = { + 'data_dir': os.path.join(base_dir, 'data-volume'), + 'tf_input_var': 'convolution2d_input_1:0', + 'tf_predict_var': 'Softmax:0', +} diff --git a/picasso/examples/tensorflow/model.py b/picasso/examples/tensorflow/model.py index f044411..b79d7ee 100644 --- a/picasso/examples/tensorflow/model.py +++ b/picasso/examples/tensorflow/model.py @@ -1,6 +1,5 @@ -from PIL import Image -from operator import itemgetter import numpy as np +from PIL import Image from picasso.ml_frameworks.tensorflow.model import TFModel @@ -10,45 +9,28 @@ class TensorflowMNISTModel(TFModel): - @staticmethod - def preprocess(targets): + def preprocess(self, raw_inputs): + """Convert images into the format required by our model. + + Our model requires that inputs be grayscale (mode 'L'), be resized to + `MNIST_DIM`, and be represented as float32 numpy arrays in range + [0, 1]. + + Args: + raw_inputs (list of Images): a list of PIL Image objects + + Returns: + array (float32): num images * height * width * num channels + + """ image_arrays = [] - for target in targets: - im = target.convert('L') + for raw_im in raw_inputs: + im = raw_im.convert('L') im = im.resize(MNIST_DIM, Image.ANTIALIAS) arr = np.array(im) image_arrays.append(arr) - all_targets = np.array(image_arrays) - return all_targets.reshape(len(all_targets), - MNIST_DIM[0], - MNIST_DIM[1], 1).astype('float32') / 255 - - @staticmethod - def postprocess(output_arr): - images = [] - for row in output_arr: - im_array = row.reshape(MNIST_DIM) - images.append(im_array) - - return images - - @staticmethod - def prob_decode(probability_array, top=5): - results = [] - for row in probability_array: - entries = [] - for i, prob in enumerate(row): - entries.append({'index': i, - 'name': str(i), - 'prob': prob}) - - entries = sorted(entries, - key=itemgetter('prob'), - reverse=True)[:top] - - for entry in entries: - entry['prob'] = '{:.3f}'.format(entry['prob']) - results.append(entries) - - return results + inputs = np.array(image_arrays) + return inputs.reshape(len(inputs), + MNIST_DIM[0], + MNIST_DIM[1], 1).astype('float32') / 255 diff --git a/picasso/ml_frameworks/keras/model.py b/picasso/ml_frameworks/keras/model.py index e88aacf..d122a89 100644 --- a/picasso/ml_frameworks/keras/model.py +++ b/picasso/ml_frameworks/keras/model.py @@ -1,7 +1,7 @@ -import os +from datetime import datetime import glob import json -from datetime import datetime +import os import keras.backend as K from keras.models import model_from_json, load_model @@ -10,20 +10,16 @@ class KerasModel(BaseModel): - """Implements model loading functions for Keras - Using this Keras module will require the h5py library, - which is not included with Keras - Attributes: - sess (Tensorflow :obj:`Session`): underlying Tensorflow session of - the Keras model. - tf_predict_var (:obj:`Tensor`): tensorflow tensor which represents - the class probabilities - tf_input_var (:obj:`Tensor`): tensorflow tensor which represents - the inputs + """Implements model loading functions for Keras. + + Using this Keras module will require the h5py library, which is not + included with Keras. + """ - def load(self, data_dir='./'): - """Load graph and weight data + def load(self, data_dir): + """Load graph and weight data. + Args: data_dir (:obj:`str`): location of Keras checkpoint (`.hdf5`) files and model (in `.json`) structure. The default behavior @@ -35,13 +31,10 @@ def load(self, data_dir='./'): # find newest ckpt and graph files try: latest_ckpt = max(glob.iglob( - os.path.join(data_dir, '*.h*5')), - key=os.path.getctime) - self.latest_ckpt_name = os.path.basename(latest_ckpt) - self.latest_ckpt_time = str(datetime.fromtimestamp( - os.path.getmtime(latest_ckpt)) - ) - + os.path.join(data_dir, '*.h*5')), key=os.path.getctime) + latest_ckpt_name = os.path.basename(latest_ckpt) + latest_ckpt_time = str( + datetime.fromtimestamp(os.path.getmtime(latest_ckpt))) except ValueError: raise FileNotFoundError('No checkpoint (.hdf5 or .h5) files ' 'available at {}'.format(data_dir)) @@ -50,23 +43,23 @@ def load(self, data_dir='./'): key=os.path.getctime) with open(latest_json, 'r') as f: model_json = json.loads(f.read()) - self.model = model_from_json(model_json) + self._model = model_from_json(model_json) - self.model.load_weights(latest_ckpt) + self._model.load_weights(latest_ckpt) except ValueError: try: - self.model = load_model(latest_ckpt) - + self._model = load_model(latest_ckpt) except ValueError: raise FileNotFoundError('The (.hdf5 or .h5) files available at' '{} don\'t have the model' ' architecture.' .format(latest_ckpt)) - self.sess = K.get_session() - - self.tf_predict_var = self.model.outputs[0] - self.tf_input_var = self.model.inputs[0] + self._sess = K.get_session() + self._tf_predict_var = self._model.outputs[0] + self._tf_input_var = self._model.inputs[0] + self._description = "%s loaded from %s (name: %s, timestamp: %s)" % ( + type(self).__name__, data_dir, latest_ckpt_name, latest_ckpt_time) - def _predict(self, input_array): - return self.model.predict(input_array) + def predict(self, input_array): + return self._model.predict(input_array) diff --git a/picasso/ml_frameworks/model.py b/picasso/ml_frameworks/model.py index d876655..10b8acc 100644 --- a/picasso/ml_frameworks/model.py +++ b/picasso/ml_frameworks/model.py @@ -1,191 +1,187 @@ -import warnings -from operator import itemgetter import importlib +from operator import itemgetter +import warnings + + +def load_model(model_cls_path, model_cls_name, model_load_args): + """Get an instance of the described model. + + Args: + model_cls_path: Path to the module in which the model class + is defined. + model_cls_name: Name of the model class. + model_load_args: Dictionary of args to pass to the `load` method + of the model instance. + + Returns: + An instance of :class:`.ml_frameworks.model.BaseModel` or subclass + + """ + spec = importlib.util.spec_from_file_location('active_model', + model_cls_path) + model_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(model_module) + model_cls = getattr(model_module, model_cls_name) + model = model_cls() + if not isinstance(model, BaseModel): + warnings.warn("Loaded model '%s' at '%s' is not an instance of %r" + % (model_cls_name, model_cls_path, BaseModel)) + model.load(**model_load_args) + return model class BaseModel: - """Model class interface. + """Interface encapsulating a trained NN model usable for prediction. - All ML frameworks should derive from this class for the purposes of - the visualization. This class loads saved files generated by various - ML frameworks and allows us to extract the graph topology, weights, etc. + This interface defines: + + - How to load the model's topology and parameters from disk + - How to preprocess a batch of examples for the model + - How to perform prediction using the model + - Etc """ def __init__(self, - top_probs=5, - **kwargs): - """Attempt to load utilities + top_probs=5): + """Create a new instance of this model. - The class constructor attempts to import a preprocessor, postprocessor, - and probability decoder if a path is supplied. + `BaseModel` is an interface and should only be instantiated via a + subclass. Args: top_probs (int): Number of classes to display per result. For instance, VGG16 has 1000 classes, we don't want to display a visualization for every single possibility. Defaults to 5. - **kwargs: Arbitrary keyword arguments, useful for passing specific - settings to derived classes. - """ - self.latest_ckpt_name = None - self.latest_ckpt_time = None self.top_probs = top_probs - if kwargs: - for key, value in kwargs.items(): - setattr(self, key, value) - def load(self, data_dir, **kwargs): - """Load the model in the desired framework + self._sess = None + self._tf_input_var = None + self._tf_predict_var = None + self._description = "%s (not yet loaded)" % type(self).__name__ - Given a directory where model data (weights and graph - structure), should be able to restore the model locally to the point - where it can be evaluated. + def load(self, *args, **kwargs): + """Load the model's graph and parameters from disk, restoring the model + into `self._sess` so that it can be run for inference. - Args: - data_dir (:obj:`str`): full path to directory containing - weight and graph data - **kwargs: Arbitrary keyword arguments, useful for passing specific - settings to derived classes. + Subclasses should set the instance variables [self._sess, + self._tf_input_var, self._tf_predict_var, self._description] in their + implementation. """ raise NotImplementedError - def _predict(self, targets): - """Evaluate new examples and return class probablilites + @property + def sess(self): + """Tensorflow session that can be used to evaluate tensors in the + model. - Given an iterable of examples or numpy array where the first - dimension is the number of example, return a n_examples x - n_classes array of class predictions + (:obj:`tf.Session`) - Args: - targets: iterable of arrays suitable for input into graph + """ + return self._sess - Returns: - array of class probabilities + @property + def tf_input_var(self): + """Tensorflow tensor that represents the model's inputs. + + (:obj:`tf.Tensor`) """ - raise NotImplementedError + return self._tf_input_var - def predict(self, raw_targets): - """Predict from raw data + @property + def tf_predict_var(self): + """Tensorflow tensor that represents the model's predicted class + probabilities. - Takes an iterable of data in its raw format. Passes to the - preprocessor and then the child class _predict. + (:obj:`tf.Tensor`) - Args: - raw_targets (:obj:`list` of :obj:`PIL.Image`): the images - to be processed + """ + return self._tf_predict_var - Returns: - array of class probabilities + @property + def description(self): + """Description of the loaded model. + + This description is rendered to the user in the UI. """ - return self._predict(self.preprocess(raw_targets)) + return self._description - def preprocess(self, raw_targets): - """Preprocess raw input for evaluation by model + def preprocess(self, raw_inputs): + """Preprocess raw inputs into the format required by the model. - Usually, input will need some preprocessing before submission - to a computation graph. For instance, the raw image may need - to converted to a numpy array of appropriate dimension + E.g, the raw image may need to converted to a numpy array of the + appropriate dimension. + + By default, we perform no preprocessing. Args: - raw_targets (:obj:`list` of :obj:`PIL.Image`): the images - to be processed + raw_inputs (:obj:`list` of :obj:`PIL.Image`): List of raw + input images of any mode and shape. Returns: - iterable of arrays of the correct shape for input into graph + array (float32): Images ready to be fed into the model. """ - try: - return getattr(self.preprocessor, - self.preprocessor_name)(raw_targets) - except AttributeError: - warnings.warn('Evaluating without preprocessor') - return raw_targets + return raw_inputs - def postprocess(self, output_arr): - """Postprocess prediction results back into images + def predict(self, inputs): + """Given preprocessed inputs, generate class probabilities by using the + model to perform inference. - Sometimes it's useful to display an intermediate computation - as image. This is model-dependent. + Given an iterable of examples or numpy array where the first + dimension is the number of example, return a n_examples x + n_classes array of class predictions Args: - output_arr (iterable of arrays): any array with the - same total number of entries an input array + inputs: Iterable of examples (e.g., a numpy array whose first + dimension is the batch size). Returns: - iterable of arrays in original image shape + Class probabilities for each input example, as a numpy array of + shape (num_examples, num_classes). """ + raise NotImplementedError - try: - return getattr(self.postprocessor, - self.postprocessor_name)(output_arr) - except AttributeError: - warnings.warn('Evaluating without postprocessor') - return output_arr + def decode_prob(self, class_probabilities): + """Given predicted class probabilites for a set of examples, annotate + each logit with a class name. - def decode_prob(self, output_arr): - """Label class probabilites with class names + By default, we name each class using its index in the logits array. Args: - output_arr (array): class probabilities + class_probabilities (array): Class probabilities as output by + `self.predict`, i.e., a numpy array of shape (num_examples, + num_classes). Returns: - result list of dict in the format [{'index': class_index, 'name': - class_name, 'prob': class_probability}, ...] + Annotated class probabilities for each input example, as a list of + dicts where each dict is formatted as: + { + 'index': class_index, + 'name': class_name, + 'prob': class_probability + } """ - - try: - return getattr(self.prob_decoder, - self.prob_decoder_name)(output_arr, - top=self.top_probs) - except AttributeError: - warnings.warn('Evaluating without class decoder') - results = [] - for row in output_arr: - entries = [] - for i, prob in enumerate(row): - entries.append({'index': i, - 'name': str(i), - 'prob': prob}) - - entries = sorted(entries, - key=itemgetter('prob'), - reverse=True)[:self.top_probs] - - for entry in entries: - entry['prob'] = '{:.3f}'.format(entry['prob']) - results.append(entries) - return results - - -def generate_model(model_cls_path, model_cls_name, **kwargs): - """Get an instance of the described model. - - Args: - model_cls_path: Path to the module in which the model class - is defined. - model_cls_name: Name of the model class. - data_dir: Directory containing the graph and weights. - kwargs: Arbitrary keyword arguments passed to the model's - constructor. - - Returns: - An instance of :class:`.ml_frameworks.model.BaseModel` or subclass - - """ - spec = importlib.util.spec_from_file_location('active_model', - model_cls_path) - model_module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(model_module) - model_cls = getattr(model_module, model_cls_name) - model = model_cls(**kwargs) - if not isinstance(model, BaseModel): - warnings.warn("Loaded model '%s' at '%s' is not an instance of %r" - % (model_cls_name, model_cls_path, BaseModel)) - return model + results = [] + for row in class_probabilities: + entries = [] + for i, prob in enumerate(row): + entries.append({'index': i, + 'name': str(i), + 'prob': prob}) + + entries = sorted(entries, + key=itemgetter('prob'), + reverse=True)[:self.top_probs] + + for entry in entries: + entry['prob'] = '{:.3f}'.format(entry['prob']) + results.append(entries) + return results diff --git a/picasso/ml_frameworks/tensorflow/model.py b/picasso/ml_frameworks/tensorflow/model.py index 2689808..61cf5b5 100644 --- a/picasso/ml_frameworks/tensorflow/model.py +++ b/picasso/ml_frameworks/tensorflow/model.py @@ -1,6 +1,6 @@ -import os -import glob from datetime import datetime +import glob +import os import tensorflow as tf @@ -8,30 +8,26 @@ class TFModel(BaseModel): - """Implements model loading functions for tensorflow""" - - # Name of the tensor corresponding to the model's inputs. You must define - # this if you are loading the model from a checkpoint. - tf_input_var = None + """Implements model loading functions for Tensorflow. + + """ - # Name of the tensor corresponding to the model's inputs. You must define - # this if you are loading the model from a checkpoint. - tf_predict_var = None - - def load(self, data_dir='./'): - """Load graph and weight data + def load(self, data_dir, tf_input_var=None, tf_predict_var=None): + """Load graph and weight data. Args: - data_dir (:obj:`str`): location of tensorflow checkpoint - data. We'll need the .meta file to reconstruct - the graph and the data (checkpoint) files to - fill in the weights of the model. The default - behavior is take the latest files, by OS timestamp. + data_dir (:obj:`str`): location of tensorflow checkpoint data. + We'll need the .meta file to reconstruct the graph and the data + (checkpoint) files to fill in the weights of the model. The + default behavior is take the latest files, by OS timestamp. + tf_input_var (:obj:`str`): Name of the tensor corresponding to the + model's inputs. You must define this if you are loading the + model from a checkpoint. + tf_predict_var (:obj:`str`): Name of the tensor corresponding to the + model's predictions. You must define this if you are loading + the model from a checkpoint. """ - - self.sess = tf.Session() - self.sess.as_default() # find newest ckpt and meta files try: latest_ckpt_fn = max( @@ -40,14 +36,11 @@ def load(self, data_dir='./'): # timestamps lambda x: os.path.splitext(x)[-1].startswith('.meta') or os.path.splitext(x)[-1].startswith('.index'), - glob.glob(os.path.join(data_dir, '*.ckpt*')) - ), + glob.glob(os.path.join(data_dir, '*.ckpt*'))), key=os.path.getctime) - self.latest_ckpt_time = str(datetime.fromtimestamp( - os.path.getmtime(latest_ckpt_fn) - )) - # remove any step info that's been appended to the - # extenstion + latest_ckpt_time = str( + datetime.fromtimestamp(os.path.getmtime(latest_ckpt_fn))) + # remove any step info that's been appended to the extension fileext_div = latest_ckpt_fn.rfind('.ckpt') additional_ext = latest_ckpt_fn.rfind('.', fileext_div + 1) if additional_ext < 0: @@ -57,6 +50,7 @@ def load(self, data_dir='./'): except ValueError: raise FileNotFoundError('No checkpoint (.ckpt) files ' 'available at {}'.format(data_dir)) + try: latest_meta = max(glob.iglob(os.path.join(data_dir, '*.meta')), key=os.path.getctime) @@ -64,15 +58,18 @@ def load(self, data_dir='./'): raise FileNotFoundError('No graph (.meta) files ' 'available at {}'.format(data_dir)) - with self.sess.as_default() as sess: - self.saver = tf.train.import_meta_graph(latest_meta) - self.saver.restore(sess, latest_ckpt) + self._sess = tf.Session() + self._sess.as_default() + + self._saver = tf.train.import_meta_graph(latest_meta) + self._saver.restore(self._sess, latest_ckpt) - self.tf_predict_var = \ - self.sess.graph.get_tensor_by_name(self.tf_predict_var) - self.tf_input_var = \ - self.sess.graph.get_tensor_by_name(self.tf_input_var) + self._tf_input_var = self._sess.graph.get_tensor_by_name(tf_input_var) + self._tf_predict_var = self._sess.graph.get_tensor_by_name( + tf_predict_var) + self._description = "%s loaded from %s (timestamp: %s)" % ( + type(self).__name__, data_dir, latest_ckpt_time) - def _predict(self, input_array): + def predict(self, input_array): return self.sess.run(self.tf_predict_var, {self.tf_input_var: input_array}) diff --git a/picasso/picasso.py b/picasso/picasso.py index 74f0859..ba2bcf1 100644 --- a/picasso/picasso.py +++ b/picasso/picasso.py @@ -48,7 +48,7 @@ from picasso import app from picasso import __version__ -from picasso.ml_frameworks.model import generate_model +from picasso.ml_frameworks.model import load_model from picasso.visualizations import BaseVisualization from picasso.visualizations import * @@ -80,17 +80,8 @@ # safest way. Would be much better to connect to a # persistent tensorflow session running in another process or # machine. -ml_backend = \ - generate_model( - app.config['MODEL_CLS_PATH'], - app.config['MODEL_CLS_NAME'], - # passes along all settings prefixed with - # "BACKEND_" without the prefix, as lowercase - **{k.lower()[8:]: v for (k, v) - in app.config.items() - if k.startswith('BACKEND')} - ) -ml_backend.load(app.config['DATA_DIR']) +model = load_model(app.config['MODEL_CLS_PATH'], app.config['MODEL_CLS_NAME'], + app.config['MODEL_LOAD_ARGS']) @app.before_request @@ -114,11 +105,23 @@ def initialize_new_session(): session['img_input_dir'] = mkdtemp() session['img_output_dir'] = mkdtemp() -def get_visualizations(): - """Get visualization classes in context - Puts the available visualizations in the request context - and returns them. +def get_model(): + """Get the NN model that's being analyzed from the request context. Put + the model in the request context if it is not yet there. + + Returns: + instance of :class:`.ml_frameworks.model.Model` or derived + class + """ + if not hasattr(g, 'model'): + g.model = model + return g.model + + +def get_visualizations(): + """Get the available visualizations from the request context. Put the + visualizations in the request context if they are not yet there. Returns: :obj:`list` of instances of :class:`.BaseVisualization` or @@ -128,26 +131,12 @@ def get_visualizations(): if not hasattr(g, 'visualizations'): g.visualizations = {} for VisClass in VISUALIZATON_CLASSES: - vis = VisClass(get_ml_backend()) + vis = VisClass(get_model()) g.visualizations[vis.__class__.__name__] = vis return g.visualizations -def get_ml_backend(): - """Get machine learning backend in context - - Puts the backend in the request context and returns it. - - Returns: - instance of :class:`.ml_frameworks.model.Model` or derived - class - """ - if not hasattr(g, 'ml_backend'): - g.ml_backend = ml_backend - return g.ml_backend - - def get_app_state(): """Get current status of application in context @@ -156,12 +145,10 @@ def get_app_state(): """ if not hasattr(g, 'app_state'): - model = get_ml_backend() + model = get_model() g.app_state = { 'app_title': APP_TITLE, - 'backend': type(model).__name__, - 'latest_ckpt_name': model.latest_ckpt_name, - 'latest_ckpt_time': model.latest_ckpt_time + 'model_description': model.description, } return g.app_state @@ -266,29 +253,28 @@ def landing(): if request.method == 'POST': session['vis_name'] = request.form.get('choice') vis = get_visualizations()[session['vis_name']] - if hasattr(vis, 'settings'): + if vis.ALLOWED_SETTINGS: return visualization_settings() return select_files() # otherwise, on GET request visualizations = get_visualizations() vis_desc = [{'name': vis, - 'description': visualizations[vis].description} + 'description': visualizations[vis].DESCRIPTION} for vis in visualizations] session.clear() return render_template('select_visualization.html', app_state=get_app_state(), visualizations=sorted(vis_desc, - key=itemgetter('name')) - ) + key=itemgetter('name'))) @app.route('/visualization_settings', methods=['POST']) def visualization_settings(): """Visualization settings page - Will only render if the visualization object has a `settings` - attribute. + Will only render if the visualization object has a non-null + `ALLOWED_SETTINGS` attribute. """ if request.method == 'POST': @@ -296,7 +282,7 @@ def visualization_settings(): return render_template('settings.html', app_state=get_app_state(), current_vis=session['vis_name'], - settings=vis.settings) + settings=vis.ALLOWED_SETTINGS) @app.route('/select_files', methods=['GET', 'POST']) @@ -345,8 +331,8 @@ def select_files(): entry['data'].save(path, 'PNG') kwargs = {} - if hasattr(vis, 'reference_link'): - kwargs.update({'reference_link': vis.reference_link}) + if vis.REFERENCE_LINK: + kwargs['reference_link'] = vis.REFERENCE_LINK return render_template('{}.html'.format(session['vis_name']), inputs=inputs, diff --git a/picasso/settings.py b/picasso/settings.py index bc9c3c8..9e8dd9d 100644 --- a/picasso/settings.py +++ b/picasso/settings.py @@ -4,23 +4,27 @@ class Default: - """Default configuration settings + """Default settings for the Flask app. - The app will use these settings if none are specified. That is, - if no configuration file is specified by PICASSO_SETTINGS - or any individual setting is specified by environment variable. - These are, in effect, "settings of last resort." + The Flask app uses these settings if no custom settings are defined. You + can define custom settings by creating a Python module, defining global + variables in that module, and setting the environment variable + `PICASSO_SETTINGS` to the path to that module. - The paths will automatically be generated based on the location of - the source. - """ + If `PICASSO_SETTINGS` is not set, or if any particular setting is not + defined in the indicated module, then the Flask app uses these default + settings. - #: :obj:`str`: filepath of the module containing the model to run - MODEL_CLS_PATH = os.path.join(base_dir, 'examples', 'keras', 'model.py') + """ + # :obj:`str`: filepath of the module containing the model to run + MODEL_CLS_PATH = os.path.join( + base_dir, 'examples', 'keras', 'model.py') - #: :obj:`str`: name of model class + # :obj:`str`: name of model class MODEL_CLS_NAME = 'KerasMNISTModel' - #: :obj:`str`: path to directory containing weights and graph - DATA_DIR = os.path.join( - base_dir, 'examples', 'keras', 'data-volume') + # :obj:`dict`: dictionary of args to pass to the `load` method of the + # model instance. + MODEL_LOAD_ARGS = { + 'data_dir': os.path.join(base_dir, 'examples', 'keras', 'data-volume'), + } diff --git a/picasso/templates/layout.html b/picasso/templates/layout.html index cd30161..6e4a271 100644 --- a/picasso/templates/layout.html +++ b/picasso/templates/layout.html @@ -10,22 +10,26 @@ +

{{ app_state.app_title }} by Merantix

-
-

Current backend: {{ app_state.backend }}

- {% if app_state.latest_ckpt_name is defined %} -

Current checkpoint: {{ app_state.latest_ckpt_name }}

- {% endif %} - {% if app_state.latest_ckpt_time is defined %} -

Last updated: {{ app_state.latest_ckpt_time }}

- {% endif %} + + -
+ +
+ {% block body %}{% endblock %} + +
+ + +
diff --git a/picasso/visualizations/__init__.py b/picasso/visualizations/__init__.py index 0146dd3..a884af8 100644 --- a/picasso/visualizations/__init__.py +++ b/picasso/visualizations/__init__.py @@ -11,32 +11,58 @@ class BaseVisualization: - """Template for visualizations - - Attributes: - description (:obj:`str`): short description of the visualization - model (instance of :class:`.ml_frameworks.model.Model` or derived class): - backend to use - settings (:obj:`dict`): a settings dictionary. Settings defined - here will be rendered in html for the user to select. See - derived classes for examples. + """Interface encapsulating a NN visualization. + + This interface defines how a visualization is computed for a given NN + model. + """ + # (:obj:`str`): Short description of the visualization. + DESCRIPTION = None + + # (:obj:`str`): Optional link to the paper specifying the visualization. + REFERENCE_LINK = None + + # (:obj:`dict`): Optional visuzalization settings that the user can select. + # Should be a dict mapping setting names to lists of their allowed values. + ALLOWED_SETTINGS = None + def __init__(self, model): - self.model = model + """Create a new instance of this visualization. + + `BaseVisualization` is an interface and should only be instantiated via + a subclass. + + Args: + model (:obj:`.ml_frameworks.model.BaseModel`): NN model to be + visualized. + + """ + self._model = model + + @property + def model(self): + """NN model to be visualized. + + (:obj:`.ml_frameworks.model.BaseModel`) + + """ + return self._model def make_visualization(self, inputs, output_dir, settings=None): - """Generate the visualization + """Generate the visualization. All visualizations must implement this method. Args: - inputs (iterable of :class:`PIL.Image`): images uploaded by the - user. Will have already been converted to :obj:`Image` - objects. - output_dir (:obj:`str`): a directory to store outputs (e.g. plots) + inputs (iterable of :class:`PIL.Image`): Batch of input images to + make visualizations for, as PIL :obj:`Image` objects. + output_dir (:obj:`str`): A directory to write outputs (e.g., + plots) to. Returns: data needed to render the visualization. Since there is an associated HTML template, the return type is arbitrary. + """ raise NotImplementedError diff --git a/picasso/visualizations/class_probabilities.py b/picasso/visualizations/class_probabilities.py index f87d5f3..ca49cbe 100644 --- a/picasso/visualizations/class_probabilities.py +++ b/picasso/visualizations/class_probabilities.py @@ -9,10 +9,9 @@ class probabilities of the input image. """ - description = 'Predict class probabilities from new examples' + DESCRIPTION = 'Predict class probabilities from new examples' - def make_visualization(self, inputs, - output_dir, settings=None): + def make_visualization(self, inputs, output_dir, settings=None): pre_processed_arrays = self.model.preprocess([example['data'] for example in inputs]) predictions = self.model.sess.run(self.model.tf_predict_var, diff --git a/picasso/visualizations/partial_occlusion.py b/picasso/visualizations/partial_occlusion.py index 239b4d6..c8b565d 100644 --- a/picasso/visualizations/partial_occlusion.py +++ b/picasso/visualizations/partial_occlusion.py @@ -22,16 +22,17 @@ class PartialOcclusion(BaseVisualization): classifying on the image feature we expect. """ - settings = { + DESCRIPTION = ('Partially occlude image to determine regions ' + 'important to classification') + + REFERENCE_LINK = 'https://arxiv.org/abs/1311.2901' + + ALLOWED_SETTINGS = { 'Window': ['0.50', '0.40', '0.30', '0.20', '0.10', '0.05'], 'Strides': ['2', '5', '10', '20', '30'], 'Occlusion': ['grey', 'black', 'white'] } - description = ('Partially occlude image to determine regions ' - 'important to classification') - reference_link = 'https://arxiv.org/abs/1311.2901' - def __init__(self, model): super(PartialOcclusion, self).__init__(model) self.predict_tensor = self.get_predict_tensor() @@ -53,11 +54,10 @@ def make_visualization(self, inputs, output_dir, settings=None): # get class predictions as in ClassProbabilities pre_processed_arrays = self.model.preprocess([example['data'] - for example in inputs]) - class_predictions = \ - self.model.sess.run(self.model.tf_predict_var, - feed_dict={self.model.tf_input_var: - pre_processed_arrays}) + for example in inputs]) + class_predictions = self.model.sess.run( + self.model.tf_predict_var, + feed_dict={self.model.tf_input_var: pre_processed_arrays}) decoded_predictions = self.model.decode_prob(class_predictions) results = [] @@ -86,11 +86,9 @@ def make_visualization(self, inputs, output_dir, settings=None): os.path.join(output_dir, example_filename), format=im_format) - filenames = \ - self.make_heatmaps(predictions, - output_dir, - example['filename'], - decoded_predictions=decoded_predictions[i]) + filenames = self.make_heatmaps( + predictions, output_dir, example['filename'], + decoded_predictions=decoded_predictions[i]) results.append({'input_filename': example['filename'], 'result_filenames': filenames, 'predict_probs': decoded_predictions[i], @@ -100,8 +98,8 @@ def make_visualization(self, inputs, output_dir, settings=None): def get_predict_tensor(self): # Assume that predict is the softmax # tensor in the computation graph - return self.model.sess.graph. \ - get_tensor_by_name(self.model.tf_predict_var.name) + return self.model.sess.graph.get_tensor_by_name( + self.model.tf_predict_var.name) def update_settings(self, settings): def error_string(setting, setting_val): @@ -112,19 +110,19 @@ def error_string(setting, setting_val): vis=self.__class__.__name__) if 'Window' in settings: - if settings['Window'] in self.settings['Window']: + if settings['Window'] in self.ALLOWED_SETTINGS['Window']: self.window = float(settings['Window']) else: raise ValueError(error_string(settings['Window'], 'Window')) if 'Strides' in settings: - if settings['Strides'] in self.settings['Strides']: + if settings['Strides'] in self.ALLOWED_SETTINGS['Strides']: self.num_windows = int(settings['Strides']) else: raise ValueError(error_string(settings['Strides'], 'Strides')) if 'Occlusion' in settings: - if settings['Occlusion'] in self.settings['Occlusion']: + if settings['Occlusion'] in self.ALLOWED_SETTINGS['Occlusion']: self.occlusion_method = settings['Occlusion'] else: raise ValueError(error_string(settings['Occlusion'], @@ -166,12 +164,9 @@ def occluded_images(self, im): win_length = round(self.window * length) pad_horizontal = win_width // 2 pad_vertical = win_length // 2 - centers_horizontal, centers_vertical = \ - self.get_centers(width, length, - win_width, win_length, - pad_horizontal, pad_vertical, - self.num_windows - ) + centers_horizontal, centers_vertical = self.get_centers( + width, length, win_width, win_length, pad_horizontal, pad_vertical, + self.num_windows) upper_left_corners = np.array( [(w - pad_vertical, v - pad_horizontal) for w in centers_vertical diff --git a/picasso/visualizations/saliency_maps.py b/picasso/visualizations/saliency_maps.py index 060df75..dcdd709 100644 --- a/picasso/visualizations/saliency_maps.py +++ b/picasso/visualizations/saliency_maps.py @@ -21,24 +21,27 @@ class SaliencyMaps(BaseVisualization): classification (as changing them would change the classification). """ - description = ('See maximal derivates against class with respect ' + DESCRIPTION = ('See maximal derivates against class with respect ' 'to input') - reference_link = 'https://arxiv.org/pdf/1312.6034' + + REFERENCE_LINK = 'https://arxiv.org/pdf/1312.6034' def __init__(self, model, logit_tensor_name=None): super(SaliencyMaps, self).__init__(model) if logit_tensor_name: - self.logit_tensor = self.model.sess.graph \ - .get_tensor_by_name(logit_tensor_name) + self.logit_tensor = self.model.sess.graph.get_tensor_by_name( + logit_tensor_name) else: self.logit_tensor = self.get_logit_tensor() + self.input_shape = self.model.tf_input_var.get_shape()[1:].as_list() + def get_gradient_wrt_class(self, class_index): - gradient_name = 'bv_{class_index}_gradient' \ - .format(class_index=class_index) + gradient_name = 'bv_{class_index}_gradient'.format( + class_index=class_index) try: - return self.model.sess.graph. \ - get_tensor_by_name('{}:0'.format(gradient_name)) + return self.model.sess.graph.get_tensor_by_name( + '{}:0'.format(gradient_name)) except KeyError: class_logit = tf.slice(self.logit_tensor, [0, class_index], @@ -61,24 +64,29 @@ def make_visualization(self, inputs, output_dir, settings=None): results = [] for i, inp in enumerate(inputs): class_gradients = [] - output_images = [] relevant_class_indices = [pred['index'] for pred in decoded_predictions[i]] - gradients_wrt_class = [self.get_gradient_wrt_class(index) for index - in relevant_class_indices] + gradients_wrt_class = [self.get_gradient_wrt_class(index) + for index in relevant_class_indices] for gradient_wrt_class in gradients_wrt_class: class_gradients.append([self.model.sess.run( gradient_wrt_class, feed_dict={self.model.tf_input_var: [arr]}) for arr in pre_processed_arrays]) - output_fns = [] - output_arrays = np.array([gradient[i] for - gradient in class_gradients]) + + output_arrays = np.array([gradient[i] + for gradient in class_gradients]) # if images are color, take the maximum channel if output_arrays.shape[-1] == 3: output_arrays = output_arrays.max(-1) + # we care about the size of the derivative, not the sign + output_arrays = np.abs(output_arrays) + + # We want each array to be represented as a 1-channel image of + # the same size as the model's input image. + output_images = output_arrays.reshape([-1] + self.input_shape[0:2]) - output_images = self.model.postprocess(np.abs(output_arrays)) + output_fns = [] for j, image in enumerate(output_images): output_fn = '{fn}-{j}-{ts}.png'.format(ts=str(time.time()), j=j, @@ -105,9 +113,9 @@ def make_visualization(self, inputs, output_dir, settings=None): def get_logit_tensor(self): # Assume that the logits are the tensor input to the last softmax # operation in the computation graph - sm = [node for node in self.model.sess.graph_def.node - if node.name == - self.model.tf_predict_var.name.split(':')[0]][-1] + sm = [node + for node in self.model.sess.graph_def.node + if node.name == self.model.tf_predict_var.name.split(':')[0]][-1] logit_op_name = sm.input[0] - return self.model.sess.graph. \ - get_tensor_by_name('{}:0'.format(logit_op_name)) + return self.model.sess.graph.get_tensor_by_name( + '{}:0'.format(logit_op_name)) diff --git a/tests/conftest.py b/tests/conftest.py index c7d66f9..9767d15 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,7 +28,6 @@ def example_prob_array(): @pytest.fixture def base_model(): from picasso.ml_frameworks.model import BaseModel - class BaseModelForTest(BaseModel): def load(self, data_dir): pass @@ -38,8 +37,4 @@ def load(self, data_dir): @pytest.fixture def tensorflow_model(): from picasso.ml_frameworks.tensorflow.model import TFModel - - tfm = TFModel(tf_predict_var='Softmax:0', - tf_input_var='convolution2d_input_1:0') - - return tfm + return TFModel() diff --git a/tests/test_picasso.py b/tests/test_picasso.py index 576a1be..66582ca 100644 --- a/tests/test_picasso.py +++ b/tests/test_picasso.py @@ -141,14 +141,16 @@ def test_saved_model(self): assert km.tf_predict_var is not None -class TestTensorflowModel: +class TestTensorflowBackend: - def test_tensorflow_model(self, client, tensorflow_model): + def test_tensorflow_backend(self, tensorflow_model): """Only tests tensorflow backend loads without error """ - data_path = os.path.join('picasso', 'examples', - 'tensorflow', 'data-volume') - tensorflow_model.load(data_path) + tensorflow_model.load( + data_dir=os.path.join('picasso', 'examples', 'tensorflow', + 'data-volume'), + tf_predict_var='Softmax:0', + tf_input_var='convolution2d_input_1:0') assert tensorflow_model.tf_predict_var is not None assert tensorflow_model.tf_input_var is not None From da65a5f4b90ad8900c24c740a4d1bb5785165394 Mon Sep 17 00:00:00 2001 From: Josh Chen Date: Wed, 5 Jul 2017 19:01:14 +0200 Subject: [PATCH 13/18] Rename ml_frameworks.*, visualizations.base --- picasso/__init__.py | 2 +- picasso/{settings.py => config.py} | 0 picasso/examples/keras-vgg16/model.py | 14 ++-- picasso/examples/keras/model.py | 3 +- picasso/examples/tensorflow/model.py | 3 +- picasso/ml_frameworks/keras/__init__.py | 0 picasso/ml_frameworks/tensorflow/__init__.py | 0 picasso/{ml_frameworks => models}/__init__.py | 0 .../model.py => models/base.py} | 2 +- .../keras/model.py => models/keras.py} | 2 +- .../model.py => models/tensorflow.py} | 2 +- picasso/picasso.py | 73 +++++++++++-------- picasso/templates/layout.html | 2 +- picasso/visualizations/__init__.py | 59 +-------------- picasso/visualizations/base.py | 62 ++++++++++++++++ picasso/visualizations/class_probabilities.py | 2 +- picasso/visualizations/partial_occlusion.py | 2 +- picasso/visualizations/saliency_maps.py | 2 +- tests/conftest.py | 4 +- tests/test_picasso.py | 20 ++--- 20 files changed, 133 insertions(+), 121 deletions(-) rename picasso/{settings.py => config.py} (100%) delete mode 100644 picasso/ml_frameworks/keras/__init__.py delete mode 100644 picasso/ml_frameworks/tensorflow/__init__.py rename picasso/{ml_frameworks => models}/__init__.py (100%) rename picasso/{ml_frameworks/model.py => models/base.py} (98%) rename picasso/{ml_frameworks/keras/model.py => models/keras.py} (97%) rename picasso/{ml_frameworks/tensorflow/model.py => models/tensorflow.py} (98%) create mode 100644 picasso/visualizations/base.py diff --git a/picasso/__init__.py b/picasso/__init__.py index f1cb90b..910a03e 100644 --- a/picasso/__init__.py +++ b/picasso/__init__.py @@ -13,7 +13,7 @@ raise SystemError('Python 3.5+ required, found {}'.format(sys.version)) app = Flask(__name__) -app.config.from_object('picasso.settings.Default') +app.config.from_object('picasso.config.Default') if os.getenv('PICASSO_SETTINGS'): app.config.from_envvar('PICASSO_SETTINGS') diff --git a/picasso/settings.py b/picasso/config.py similarity index 100% rename from picasso/settings.py rename to picasso/config.py diff --git a/picasso/examples/keras-vgg16/model.py b/picasso/examples/keras-vgg16/model.py index 10e0658..963e512 100644 --- a/picasso/examples/keras-vgg16/model.py +++ b/picasso/examples/keras-vgg16/model.py @@ -1,11 +1,8 @@ -from keras.applications.imagenet_utils import (decode_predictions, - preprocess_input) -import keras.applications.imagenet_utils +from keras.applications import imagenet_utils import numpy as np from PIL import Image -from picasso.ml_frameworks.keras.model import KerasModel - +from picasso.models.keras import KerasModel VGG16_DIM = (224, 224, 3) @@ -27,10 +24,11 @@ def preprocess(self, raw_inputs): image_arrays.append(arr) all_raw_inputs = np.array(image_arrays) - return preprocess_input(all_raw_inputs) + return imagenet_utils.preprocess_input(all_raw_inputs) def decode_prob(self, class_probabilities): - r = decode_predictions(class_probabilities, top=self.top_probs) + r = imagenet_utils.decode_predictions(class_probabilities, + top=self.top_probs) results = [ [{'code': entry[0], 'name': entry[1], @@ -38,7 +36,7 @@ def decode_prob(self, class_probabilities): for entry in row] for row in r ] - classes = keras.applications.imagenet_utils.CLASS_INDEX + classes = imagenet_utils.CLASS_INDEX class_keys = list(classes.keys()) class_values = list(classes.values()) diff --git a/picasso/examples/keras/model.py b/picasso/examples/keras/model.py index adbe03d..b6534ad 100644 --- a/picasso/examples/keras/model.py +++ b/picasso/examples/keras/model.py @@ -1,8 +1,7 @@ import numpy as np from PIL import Image -from picasso.ml_frameworks.keras.model import KerasModel - +from picasso.models.keras import KerasModel MNIST_DIM = (28, 28) diff --git a/picasso/examples/tensorflow/model.py b/picasso/examples/tensorflow/model.py index b79d7ee..3c40b0a 100644 --- a/picasso/examples/tensorflow/model.py +++ b/picasso/examples/tensorflow/model.py @@ -1,8 +1,7 @@ import numpy as np from PIL import Image -from picasso.ml_frameworks.tensorflow.model import TFModel - +from picasso.models.tensorflow import TFModel MNIST_DIM = (28, 28) diff --git a/picasso/ml_frameworks/keras/__init__.py b/picasso/ml_frameworks/keras/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/picasso/ml_frameworks/tensorflow/__init__.py b/picasso/ml_frameworks/tensorflow/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/picasso/ml_frameworks/__init__.py b/picasso/models/__init__.py similarity index 100% rename from picasso/ml_frameworks/__init__.py rename to picasso/models/__init__.py diff --git a/picasso/ml_frameworks/model.py b/picasso/models/base.py similarity index 98% rename from picasso/ml_frameworks/model.py rename to picasso/models/base.py index 10b8acc..31089b1 100644 --- a/picasso/ml_frameworks/model.py +++ b/picasso/models/base.py @@ -14,7 +14,7 @@ def load_model(model_cls_path, model_cls_name, model_load_args): of the model instance. Returns: - An instance of :class:`.ml_frameworks.model.BaseModel` or subclass + An instance of :class:`.models.model.BaseModel` or subclass """ spec = importlib.util.spec_from_file_location('active_model', diff --git a/picasso/ml_frameworks/keras/model.py b/picasso/models/keras.py similarity index 97% rename from picasso/ml_frameworks/keras/model.py rename to picasso/models/keras.py index d122a89..9cb5562 100644 --- a/picasso/ml_frameworks/keras/model.py +++ b/picasso/models/keras.py @@ -6,7 +6,7 @@ import keras.backend as K from keras.models import model_from_json, load_model -from picasso.ml_frameworks.model import BaseModel +from picasso.models.base import BaseModel class KerasModel(BaseModel): diff --git a/picasso/ml_frameworks/tensorflow/model.py b/picasso/models/tensorflow.py similarity index 98% rename from picasso/ml_frameworks/tensorflow/model.py rename to picasso/models/tensorflow.py index 61cf5b5..d8d1b74 100644 --- a/picasso/ml_frameworks/tensorflow/model.py +++ b/picasso/models/tensorflow.py @@ -4,7 +4,7 @@ import tensorflow as tf -from picasso.ml_frameworks.model import BaseModel +from picasso.models.base import BaseModel class TFModel(BaseModel): diff --git a/picasso/picasso.py b/picasso/picasso.py index ba2bcf1..fc91975 100644 --- a/picasso/picasso.py +++ b/picasso/picasso.py @@ -21,18 +21,18 @@ Attributes: APP_TITLE (:obj:`str`): Name of the application to display in the title bar - VISUALIZATON_CLASSES(:obj:`tuple` of :class:`.BaseVisualization`): + VISUALIZATION_CLASSES (:obj:`tuple` of :class:`.BaseVisualization`): Visualization classes available for rendering. """ -import os -import io -import time +from importlib import import_module import inspect -import shutil +import io +import os from operator import itemgetter +import shutil from tempfile import mkdtemp -from importlib import import_module +import time from types import ModuleType from PIL import Image @@ -46,39 +46,50 @@ ) from werkzeug.utils import secure_filename -from picasso import app from picasso import __version__ -from picasso.ml_frameworks.model import load_model -from picasso.visualizations import BaseVisualization +from picasso import app +from picasso.models.base import load_model from picasso.visualizations import * +from picasso.visualizations.base import BaseVisualization APP_TITLE = 'Picasso Visualizer' -# import visualizations classes dynamically -visualization_attr = vars( - import_module('picasso.visualizations')) -visualization_submodules = [visualization_attr[x] for x in visualization_attr - if isinstance(visualization_attr[x], ModuleType)] -VISUALIZATON_CLASSES = [] -for submodule in visualization_submodules: - members = vars(submodule) - classes = [members[x] for x in members if inspect.isclass(members[x]) and - issubclass(members[x], BaseVisualization) and - members[x] is not BaseVisualization] - VISUALIZATON_CLASSES += classes - -# Use a bogus secret key for debugging ease. No -# client information is stored, the secret key is only -# necessary for generating the session cookie. + +def _get_visualization_classes(): + """Import visualizations classes dynamically + + """ + visualization_attr = vars( + import_module('picasso.visualizations')) + visualization_submodules = [ + visualization_attr[x] + for x in visualization_attr + if isinstance(visualization_attr[x], ModuleType)] + + visualization_classes = [] + for submodule in visualization_submodules: + attrs = vars(submodule) + for attr_name in attrs: + attr = attrs[attr_name] + if (inspect.isclass(attr) + and issubclass(attr, BaseVisualization) + and attr is not BaseVisualization): + visualization_classes.append(attr) + return visualization_classes + + +VISUALIZATION_CLASSES = _get_visualization_classes() + +# Use a bogus secret key for debugging ease. No client information is stored; +# the secret key is only necessary for generating the session cookie. if app.debug: app.secret_key = '...' else: app.secret_key = os.urandom(24) -# This pattern is used in other projects with Flask and -# tensorflow, but probably isn't the most stable or -# safest way. Would be much better to connect to a -# persistent tensorflow session running in another process or +# This pattern is used in other projects with Flask and Tensorflow, but +# but probably isn't the most stable or safest way. Would be much better to +# connect to a persistent Tensorflow session running in another process or # machine. model = load_model(app.config['MODEL_CLS_PATH'], app.config['MODEL_CLS_NAME'], app.config['MODEL_LOAD_ARGS']) @@ -111,7 +122,7 @@ def get_model(): the model in the request context if it is not yet there. Returns: - instance of :class:`.ml_frameworks.model.Model` or derived + instance of :class:`.models.model.Model` or derived class """ if not hasattr(g, 'model'): @@ -130,7 +141,7 @@ def get_visualizations(): """ if not hasattr(g, 'visualizations'): g.visualizations = {} - for VisClass in VISUALIZATON_CLASSES: + for VisClass in VISUALIZATION_CLASSES: vis = VisClass(get_model()) g.visualizations[vis.__class__.__name__] = vis diff --git a/picasso/templates/layout.html b/picasso/templates/layout.html index 6e4a271..d71b056 100644 --- a/picasso/templates/layout.html +++ b/picasso/templates/layout.html @@ -12,7 +12,7 @@
-

{{ app_state.app_title }} +

{{ app_state.app_title }} by Merantix diff --git a/picasso/visualizations/__init__.py b/picasso/visualizations/__init__.py index a884af8..af60df9 100644 --- a/picasso/visualizations/__init__.py +++ b/picasso/visualizations/__init__.py @@ -6,63 +6,6 @@ """ import os + __all__ = [x.rpartition('.')[0] for x in os.listdir(__path__[0]) if not x.startswith('__') and x.endswith('py')] - - -class BaseVisualization: - """Interface encapsulating a NN visualization. - - This interface defines how a visualization is computed for a given NN - model. - - """ - # (:obj:`str`): Short description of the visualization. - DESCRIPTION = None - - # (:obj:`str`): Optional link to the paper specifying the visualization. - REFERENCE_LINK = None - - # (:obj:`dict`): Optional visuzalization settings that the user can select. - # Should be a dict mapping setting names to lists of their allowed values. - ALLOWED_SETTINGS = None - - def __init__(self, model): - """Create a new instance of this visualization. - - `BaseVisualization` is an interface and should only be instantiated via - a subclass. - - Args: - model (:obj:`.ml_frameworks.model.BaseModel`): NN model to be - visualized. - - """ - self._model = model - - @property - def model(self): - """NN model to be visualized. - - (:obj:`.ml_frameworks.model.BaseModel`) - - """ - return self._model - - def make_visualization(self, inputs, output_dir, settings=None): - """Generate the visualization. - - All visualizations must implement this method. - - Args: - inputs (iterable of :class:`PIL.Image`): Batch of input images to - make visualizations for, as PIL :obj:`Image` objects. - output_dir (:obj:`str`): A directory to write outputs (e.g., - plots) to. - - Returns: - data needed to render the visualization. Since there is an - associated HTML template, the return type is arbitrary. - - """ - raise NotImplementedError diff --git a/picasso/visualizations/base.py b/picasso/visualizations/base.py new file mode 100644 index 0000000..34d0114 --- /dev/null +++ b/picasso/visualizations/base.py @@ -0,0 +1,62 @@ +class BaseVisualization: + """Interface encapsulating a NN visualization. + + This interface defines how a visualization is computed for a given NN + model. + + """ + # (:obj:`str`): Short description of the visualization. + DESCRIPTION = None + + # (:obj:`str`): Optional link to the paper specifying the visualization. + REFERENCE_LINK = None + + # (:obj:`dict`): Optional visualization settings that the user can select, + # as a dict mapping setting names to lists of their allowed values. + ALLOWED_SETTINGS = None + + def __init__(self, model): + """Create a new instance of this visualization. + + `BaseVisualization` is an interface and should only be instantiated via + a subclass. + + Args: + model (:obj:`.models.model.BaseModel`): NN model to be + visualized. + + """ + self._model = model + + @property + def model(self): + """NN model to be visualized. + + (:obj:`.models.model.BaseModel`) + + """ + return self._model + + def make_visualization(self, inputs, output_dir, settings=None): + """Generate the visualization. + + All visualizations must implement this method. + + Args: + inputs (iterable of :class:`PIL.Image`): Batch of input images to + make visualizations for, as PIL :obj:`Image` objects. + output_dir (:obj:`str`): A directory to write outputs (e.g., + plots) to. + settings (:obj:`str`): Dictionary of settings that the user + selected, as a dict mapping setting names to values. This + should only be provided if this class's `ALLOWED_SETTINGS` + attribute is non-null. + + Returns: + Object used to render the visualization, passed directly to the + visualization class's associated HTML template. Since this HTML + template is custom for each visualization class, the return type + is arbitrary. + + """ + raise NotImplementedError diff --git a/picasso/visualizations/class_probabilities.py b/picasso/visualizations/class_probabilities.py index ca49cbe..dff61a5 100644 --- a/picasso/visualizations/class_probabilities.py +++ b/picasso/visualizations/class_probabilities.py @@ -1,4 +1,4 @@ -from picasso.visualizations import BaseVisualization +from picasso.visualizations.base import BaseVisualization class ClassProbabilities(BaseVisualization): diff --git a/picasso/visualizations/partial_occlusion.py b/picasso/visualizations/partial_occlusion.py index c8b565d..9a79e16 100644 --- a/picasso/visualizations/partial_occlusion.py +++ b/picasso/visualizations/partial_occlusion.py @@ -8,7 +8,7 @@ matplotlib.use('Agg') from matplotlib import pyplot -from picasso.visualizations import BaseVisualization +from picasso.visualizations.base import BaseVisualization class PartialOcclusion(BaseVisualization): diff --git a/picasso/visualizations/saliency_maps.py b/picasso/visualizations/saliency_maps.py index dcdd709..87bd078 100644 --- a/picasso/visualizations/saliency_maps.py +++ b/picasso/visualizations/saliency_maps.py @@ -8,7 +8,7 @@ matplotlib.use('Agg') from matplotlib import pyplot -from picasso.visualizations import BaseVisualization +from picasso.visualizations.base import BaseVisualization class SaliencyMaps(BaseVisualization): diff --git a/tests/conftest.py b/tests/conftest.py index 9767d15..f35d84d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,7 +27,7 @@ def example_prob_array(): @pytest.fixture def base_model(): - from picasso.ml_frameworks.model import BaseModel + from picasso.models.base import BaseModel class BaseModelForTest(BaseModel): def load(self, data_dir): pass @@ -36,5 +36,5 @@ def load(self, data_dir): @pytest.fixture def tensorflow_model(): - from picasso.ml_frameworks.tensorflow.model import TFModel + from picasso.models.tensorflow import TFModel return TFModel() diff --git a/tests/test_picasso.py b/tests/test_picasso.py index 66582ca..8bcfae3 100644 --- a/tests/test_picasso.py +++ b/tests/test_picasso.py @@ -7,9 +7,9 @@ Tests for `picasso` module. """ -import os import io import json +import os from flask import url_for import pytest @@ -17,18 +17,18 @@ class TestWebApp: - from picasso.picasso import VISUALIZATON_CLASSES + from picasso.picasso import VISUALIZATION_CLASSES def test_landing_page_get(self, client): assert client.get(url_for('landing')).status_code == 200 - @pytest.mark.parametrize("vis", VISUALIZATON_CLASSES) + @pytest.mark.parametrize("vis", VISUALIZATION_CLASSES) def test_landing_page_post(self, client, vis): rv = client.post(url_for('landing'), data=dict(choice=vis.__name__)) assert rv.status_code == 200 - @pytest.mark.parametrize("vis", VISUALIZATON_CLASSES) + @pytest.mark.parametrize("vis", VISUALIZATION_CLASSES) def test_settings_page(self, client, vis): if hasattr(vis, 'settings'): with client.session_transaction() as sess: @@ -36,14 +36,14 @@ def test_settings_page(self, client, vis): rv = client.post(url_for('visualization_settings')) assert rv.status_code == 200 - @pytest.mark.parametrize("vis", VISUALIZATON_CLASSES) + @pytest.mark.parametrize("vis", VISUALIZATION_CLASSES) def test_file_selection_get(self, client, vis): with client.session_transaction() as sess: sess['vis_name'] = vis.__name__ rv = client.get(url_for('select_files')) assert rv.status_code == 200 - @pytest.mark.parametrize("vis", VISUALIZATON_CLASSES) + @pytest.mark.parametrize("vis", VISUALIZATION_CLASSES) def test_file_selection_post(self, client, vis, random_image_files): with client.session_transaction() as sess: sess['vis_name'] = vis.__name__ @@ -66,7 +66,7 @@ def test_file_selection_post(self, client, vis, random_image_files): class TestRestAPI: - from picasso.picasso import VISUALIZATON_CLASSES + from picasso.picasso import VISUALIZATION_CLASSES def test_api_root_get(self, client): assert client.get(url_for('api_root')).status_code == 200 @@ -84,7 +84,7 @@ def test_api_uploading_file(self, client, random_image_files): assert type(data['file']) is str assert type(data['uid']) is int - @pytest.mark.parametrize("vis", VISUALIZATON_CLASSES) + @pytest.mark.parametrize("vis", VISUALIZATION_CLASSES) def test_api_visualizing_input(self, client, random_image_files, vis): upload_file = str(random_image_files.listdir()[0]) with open(upload_file, "rb") as imageFile: @@ -124,7 +124,7 @@ class TestKerasModel: def test_saved_model(self): # tests that KerasModel can load from a saved model import tempfile - from picasso.ml_frameworks.keras.model import KerasModel + from picasso.models.keras import KerasModel data_path = os.path.join('picasso', 'examples', 'keras', 'data-volume') @@ -133,7 +133,7 @@ def test_saved_model(self): km.load(data_path) temp = tempfile.mkdtemp() - km.model.save(os.path.join(temp, 'temp.h5')) + km._model.save(os.path.join(temp, 'temp.h5')) km = KerasModel() km.load(temp) From 1a17ecf03f5bf5e0cb7a8daf3067da179cc671d6 Mon Sep 17 00:00:00 2001 From: Josh Chen Date: Thu, 6 Jul 2017 10:58:04 +0200 Subject: [PATCH 14/18] remove path from description --- picasso/models/keras.py | 4 ++-- picasso/models/tensorflow.py | 4 ++-- picasso/templates/layout.html | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/picasso/models/keras.py b/picasso/models/keras.py index 9cb5562..a059b20 100644 --- a/picasso/models/keras.py +++ b/picasso/models/keras.py @@ -58,8 +58,8 @@ def load(self, data_dir): self._sess = K.get_session() self._tf_predict_var = self._model.outputs[0] self._tf_input_var = self._model.inputs[0] - self._description = "%s loaded from %s (name: %s, timestamp: %s)" % ( - type(self).__name__, data_dir, latest_ckpt_name, latest_ckpt_time) + self._description = "%s loaded from checkpoint w/ filename=%s, timestamp=%s" % ( + type(self).__name__, latest_ckpt_name, latest_ckpt_time) def predict(self, input_array): return self._model.predict(input_array) diff --git a/picasso/models/tensorflow.py b/picasso/models/tensorflow.py index d8d1b74..092d36d 100644 --- a/picasso/models/tensorflow.py +++ b/picasso/models/tensorflow.py @@ -67,8 +67,8 @@ def load(self, data_dir, tf_input_var=None, tf_predict_var=None): self._tf_input_var = self._sess.graph.get_tensor_by_name(tf_input_var) self._tf_predict_var = self._sess.graph.get_tensor_by_name( tf_predict_var) - self._description = "%s loaded from %s (timestamp: %s)" % ( - type(self).__name__, data_dir, latest_ckpt_time) + self._description = "%s loaded from checkpoint w/ timestamp=%s" % ( + type(self).__name__, latest_ckpt_time) def predict(self, input_array): return self.sess.run(self.tf_predict_var, diff --git a/picasso/templates/layout.html b/picasso/templates/layout.html index d71b056..c241921 100644 --- a/picasso/templates/layout.html +++ b/picasso/templates/layout.html @@ -18,6 +18,10 @@

{{ app_state.app_title }}

+
+

Current model: {{ app_state.model_description }}

+
+ @@ -28,8 +32,4 @@

{{ app_state.app_title }}
- -

From 88ea820ac8f20b3613b319f25561dbf32e9a5841 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 6 Jul 2017 11:05:01 +0200 Subject: [PATCH 15/18] cleanup docstrings and minor formatting --- picasso/models/base.py | 8 +++++--- picasso/models/tensorflow.py | 8 ++++---- picasso/visualizations/class_probabilities.py | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/picasso/models/base.py b/picasso/models/base.py index 31089b1..90f2214 100644 --- a/picasso/models/base.py +++ b/picasso/models/base.py @@ -78,7 +78,7 @@ def sess(self): """Tensorflow session that can be used to evaluate tensors in the model. - (:obj:`tf.Session`) + :type: :obj:`tf.Session` """ return self._sess @@ -87,7 +87,7 @@ def sess(self): def tf_input_var(self): """Tensorflow tensor that represents the model's inputs. - (:obj:`tf.Tensor`) + :type: :obj:`tf.Tensor` """ return self._tf_input_var @@ -97,7 +97,7 @@ def tf_predict_var(self): """Tensorflow tensor that represents the model's predicted class probabilities. - (:obj:`tf.Tensor`) + :type: :obj:`tf.Tensor` """ return self._tf_predict_var @@ -108,6 +108,8 @@ def description(self): This description is rendered to the user in the UI. + :type: str + """ return self._description diff --git a/picasso/models/tensorflow.py b/picasso/models/tensorflow.py index d8d1b74..c6f38a9 100644 --- a/picasso/models/tensorflow.py +++ b/picasso/models/tensorflow.py @@ -9,7 +9,7 @@ class TFModel(BaseModel): """Implements model loading functions for Tensorflow. - + """ def load(self, data_dir, tf_input_var=None, tf_predict_var=None): @@ -23,9 +23,9 @@ def load(self, data_dir, tf_input_var=None, tf_predict_var=None): tf_input_var (:obj:`str`): Name of the tensor corresponding to the model's inputs. You must define this if you are loading the model from a checkpoint. - tf_predict_var (:obj:`str`): Name of the tensor corresponding to the - model's predictions. You must define this if you are loading - the model from a checkpoint. + tf_predict_var (:obj:`str`): Name of the tensor corresponding to + the model's predictions. You must define this if you are + loading the model from a checkpoint. """ # find newest ckpt and meta files diff --git a/picasso/visualizations/class_probabilities.py b/picasso/visualizations/class_probabilities.py index dff61a5..b1299ad 100644 --- a/picasso/visualizations/class_probabilities.py +++ b/picasso/visualizations/class_probabilities.py @@ -13,7 +13,7 @@ class probabilities of the input image. def make_visualization(self, inputs, output_dir, settings=None): pre_processed_arrays = self.model.preprocess([example['data'] - for example in inputs]) + for example in inputs]) predictions = self.model.sess.run(self.model.tf_predict_var, feed_dict={self.model.tf_input_var: pre_processed_arrays}) From 9ce855fb5e861fa963a14eb7f817ecd6279f0c94 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 6 Jul 2017 17:35:37 +0200 Subject: [PATCH 16/18] restore header --- picasso/models/base.py | 19 ++++++++++++++----- picasso/models/keras.py | 5 +++-- picasso/models/tensorflow.py | 5 +++-- picasso/picasso.py | 4 +++- picasso/templates/layout.html | 24 ++++++++++-------------- 5 files changed, 33 insertions(+), 24 deletions(-) diff --git a/picasso/models/base.py b/picasso/models/base.py index 90f2214..c4b32d2 100644 --- a/picasso/models/base.py +++ b/picasso/models/base.py @@ -60,7 +60,9 @@ def __init__(self, self._sess = None self._tf_input_var = None self._tf_predict_var = None - self._description = "%s (not yet loaded)" % type(self).__name__ + self._model_name = None + self._latest_ckpt_name = None + self._latest_ckpt_time = None def load(self, *args, **kwargs): """Load the model's graph and parameters from disk, restoring the model @@ -103,15 +105,22 @@ def tf_predict_var(self): return self._tf_predict_var @property - def description(self): - """Description of the loaded model. + def latest_ckpt_time(self): + """Timestamp of the latest checkpoint - This description is rendered to the user in the UI. + :type: str + + """ + return self._latest_ckpt_time + + @property + def latest_ckpt_name(self): + """Filename of the checkpoint :type: str """ - return self._description + return self._latest_ckpt_name def preprocess(self, raw_inputs): """Preprocess raw inputs into the format required by the model. diff --git a/picasso/models/keras.py b/picasso/models/keras.py index a059b20..b62a5a1 100644 --- a/picasso/models/keras.py +++ b/picasso/models/keras.py @@ -58,8 +58,9 @@ def load(self, data_dir): self._sess = K.get_session() self._tf_predict_var = self._model.outputs[0] self._tf_input_var = self._model.inputs[0] - self._description = "%s loaded from checkpoint w/ filename=%s, timestamp=%s" % ( - type(self).__name__, latest_ckpt_name, latest_ckpt_time) + self._model_name = type(self).__name__ + self._latest_ckpt_name = latest_ckpt_name + self._latest_ckpt_time = latest_ckpt_time def predict(self, input_array): return self._model.predict(input_array) diff --git a/picasso/models/tensorflow.py b/picasso/models/tensorflow.py index 9d3a6f1..4be7c71 100644 --- a/picasso/models/tensorflow.py +++ b/picasso/models/tensorflow.py @@ -67,8 +67,9 @@ def load(self, data_dir, tf_input_var=None, tf_predict_var=None): self._tf_input_var = self._sess.graph.get_tensor_by_name(tf_input_var) self._tf_predict_var = self._sess.graph.get_tensor_by_name( tf_predict_var) - self._description = "%s loaded from checkpoint w/ timestamp=%s" % ( - type(self).__name__, latest_ckpt_time) + self._model_name = type(self).__name__ + self._latest_ckpt_name = latest_ckpt_fn + self._latest_ckpt_time = latest_ckpt_time def predict(self, input_array): return self.sess.run(self.tf_predict_var, diff --git a/picasso/picasso.py b/picasso/picasso.py index fc91975..85f4521 100644 --- a/picasso/picasso.py +++ b/picasso/picasso.py @@ -159,7 +159,9 @@ def get_app_state(): model = get_model() g.app_state = { 'app_title': APP_TITLE, - 'model_description': model.description, + 'model_name': type(model).__name__, + 'latest_ckpt_name': model.latest_ckpt_name, + 'latest_ckpt_time': model.latest_ckpt_time } return g.app_state diff --git a/picasso/templates/layout.html b/picasso/templates/layout.html index c241921..a6444ab 100644 --- a/picasso/templates/layout.html +++ b/picasso/templates/layout.html @@ -10,26 +10,22 @@ -
-

{{ app_state.app_title }} +

{{ app_state.app_title }} by Merantix

- -
-

Current model: {{ app_state.model_description }}

-
- -
+
+

Current backend: {{ app_state.model_name }}

+ {% if app_state.latest_ckpt_name is defined %} +

Current checkpoint: {{ app_state.latest_ckpt_name }}

+ {% endif %} + {% if app_state.latest_ckpt_time is defined %} +

Last updated: {{ app_state.latest_ckpt_time }}

+ {% endif %} Start over
- -
- +
{% block body %}{% endblock %} - -
-
From dc02c410df5666d48c4b7fefaa3aa0b8053773dd Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 6 Jul 2017 19:11:09 +0200 Subject: [PATCH 17/18] update documentation --- docs/models.rst | 90 +++++++++++++++++++---------------------- docs/settings.rst | 37 ++++++----------- docs/visualizations.rst | 31 +++++--------- 3 files changed, 65 insertions(+), 93 deletions(-) diff --git a/docs/models.rst b/docs/models.rst index 8dd54ea..671e71e 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -12,19 +12,18 @@ We include three `examples`_ for you to try: a model trained on the `MNIST`_ dat If you built your model with Keras using a `Sequential`_ model, you should be more or less good to go. If you used Tensorflow, you'll need to manually specify the entry and exit points [#]_. -You can specify the backend (Tensorflow or Keras) using the ``PICASSO_BACKEND_ML`` setting. The allowed values are ``tensorflow`` or ``keras`` (see :doc:`settings`). - Your model data =============== -You can specify the data directory with the ``PICASSO_DATA_DIR`` setting. This directory should contain the Keras or Tensorflow checkpoint files. If multiple checkpoints are found, the latest one will be used (see example `Keras model code`_). +You can specify the data directory with the ``MODEL_LOAD_ARGS.data_dir`` setting (see :doc:`settings`). This directory should contain the Keras or Tensorflow checkpoint files. If multiple checkpoints are found, the latest one will be used (see example `Keras model code`_). Utility functions ================= In addition to the graph and weight information of the model itself, you'll need to define a few functions to help the visualization interact with user input, and interpret raw output from your computational graph. These are arbitrary python functions, and their locations can be specified in the :doc:`settings`. -We'll draw from the `Keras MNIST example`_ for this guide. +We'll draw from the `Keras MNIST example`_ for this guide. All custom models +from the relevant model: either ``KerasModel`` or ``TensorflowModel``. Preprocessor ------------ @@ -33,39 +32,29 @@ The preprocessor takes images uploaded to the webapp and converts them into arra .. code-block:: python3 - MNIST_DIM = (28, 28) - - def preprocess(targets): - image_arrays = [] - for target in targets: - im = target.convert('L') - im = im.resize(MNIST_DIM, Image.ANTIALIAS) - arr = np.array(im) - image_arrays.append(arr) - - all_targets = np.array(image_arrays) - return all_targets.reshape(len(all_targets), - MNIST_DIM[0], - MNIST_DIM[1], 1).astype('float32') / 255 - -Specifically, we have to convert an arbitrary input color image to a float array of the input size specified with ``MNIST_DIM``. + import numpy as np + from PIL import Image + + from picasso.models.keras import KerasModel -Postprocessor -------------- + MNIST_DIM = (28, 28) -For some visualizations, it's useful to convert a flat representation back into an array with the same shape as the original image. + class KerasMNISTModel(KerasModel): -.. code-block:: python3 + def preprocess(self, raw_inputs): + image_arrays = [] + for target in targets: + im = target.convert('L') + im = im.resize(MNIST_DIM, Image.ANTIALIAS) + arr = np.array(im) + image_arrays.append(arr) - def postprocess(output_arr): - images = [] - for row in output_arr: - im_array = row.reshape(MNIST_DIM) - images.append(im_array) + all_targets = np.array(image_arrays) + return all_targets.reshape(len(all_targets), + MNIST_DIM[0], + MNIST_DIM[1], 1).astype('float32') / 255 - return images - -This therefore takes an arbitrary array (with the same number of total entries as the image array) and reshapes it back. +Specifically, we have to convert an arbitrary input color image to a float array of the input size specified with ``MNIST_DIM``. Class Decoder ------------- @@ -74,24 +63,27 @@ Class probabilities are usually returned in an array. For any visualization whe .. code-block:: python3 - def prob_decode(probability_array, top=5): - results = [] - for row in probability_array: - entries = [] - for i, prob in enumerate(row): - entries.append({'index': i, - 'name': str(i), - 'prob': prob}) + class KerasMNISTModel(KerasModel): - entries = sorted(entries, - key=itemgetter('prob'), - reverse=True)[:top] + ... + + def decode_prob(self, class_probabilities): + results = [] + for row in class_probabilities: + entries = [] + for i, prob in enumerate(row): + entries.append({'index': i, + 'name': str(i), + 'prob': prob}) - for entry in entries: - entry['prob'] = '{:.3f}'.format(entry['prob']) - results.append(entries) + entries = sorted(entries, + key=itemgetter('prob'), + reverse=True)[:self.top_probs] - return results + for entry in entries: + entry['prob'] = '{:.3f}'.format(entry['prob']) + results.append(entries) + return results ``results`` is then a list of dicts in the format ``[{'index': class_index, 'name': class_name, 'prob': class_probability}, ...]``. In the case of the MNIST dataset, the index is the same as the class name (digits 0-9). @@ -103,9 +95,9 @@ Class probabilities are usually returned in an array. For any visualization whe .. _Sequential: https://keras.io/models/sequential/ -.. _Keras model code: https://github.com/merantix/picasso/blob/master/picasso/ml_frameworks/keras/model.py +.. _Keras model code: https://github.com/merantix/picasso/blob/master/picasso/keras/keras.py -.. _Keras MNIST example: https://github.com/merantix/picasso/blob/master/picasso/examples/keras/util.py +.. _Keras MNIST example: https://github.com/merantix/picasso/blob/master/picasso/examples/keras/model.py .. _PIL Image: http://pillow.readthedocs.io/en/latest/reference/Image.html diff --git a/docs/settings.rst b/docs/settings.rst index e465a4c..a2d189a 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -21,29 +21,18 @@ Tells the app to use this configuration instead of the default one. Inside base_dir = os.path.split(os.path.abspath(__file__))[0] - BACKEND_ML = 'tensorflow' - BACKEND_PREPROCESSOR_NAME = 'util' - BACKEND_PREPROCESSOR_PATH = os.path.join(base_dir, 'util.py') - BACKEND_POSTPROCESSOR_NAME = 'postprocess' - BACKEND_POSTPROCESSOR_PATH = os.path.join(base_dir, 'util.py') - BACKEND_PROB_DECODER_NAME = 'prob_decode' - BACKEND_PROB_DECODER_PATH = os.path.join(base_dir, 'util.py') - DATA_DIR = os.path.join(base_dir, 'data-volume') - -Any lowercase line is ignored for the purposes of determining a setting. These -can also be set via environment variables, but you must append the app name. -For instance ``BACKEND_ML = 'tensorflow'`` would become ``export -PICASSO_BACKEND_ML=tensorflow``. - -For explanations of each setting, see :mod:`picasso.settings`. Any -additional settings starting with `BACKEND_` will be sent to the model backend -as a keyword argument. The input and output tensor names can be passed to the -Tensorflow backend in this way: - -.. code-block:: python3 - - ... - BACKEND_TF_PREDICT_VAR='Softmax:0' - BACKEND_TF_INPUT_VAR='convolution2d_input_1:0' + MODEL_CLS_PATH = os.path.join(base_dir, 'model.py') + MODEL_CLS_NAME = 'TensorflowMNISTModel' + MODEL_LOAD_ARGS = { + 'data_dir': os.path.join(base_dir, 'data-volume'), + 'tf_input_var': 'convolution2d_input_1:0', + 'tf_predict_var': 'Softmax:0', + } + +Any lowercase line is ignored for the purposes of determining a setting. +``MODEL_LOAD_ARGS`` will pass the arguments along to the model's ``load`` +function. + +For explanations of each setting, see :mod:`picasso.config`. .. _managed by Flask: http://flask.pocoo.org/docs/latest/config/ diff --git a/docs/visualizations.rst b/docs/visualizations.rst index 86d65c1..51cd834 100644 --- a/docs/visualizations.rst +++ b/docs/visualizations.rst @@ -13,14 +13,12 @@ For our example, ``FunViz``, we'll need ``picasso/visualizations/fun_viz.py``: .. code-block:: python3 - from picasso.visualizations import BaseVisualization + from picasso.visualizations.base import BaseVisualization class FunViz(BaseVisualization): - def __init__(self, model): - self.description = 'A fun visualization!' - self.model = model + DESCRIPTION = 'A fun visualization!' def make_visualization(self, inputs, output_dir, settings=None): pass @@ -34,7 +32,7 @@ and ``picasso/templates/FunViz.html``: your visualization html goes here {% endblock %} -Some explanation for the ``FunViz`` class in ``fun_viz.py``: All visualizations should inherit from :class:`~picasso.visualizations.__init__.BaseVisualization` (see `code `_). You must implement the ``__init__`` method, and it should accept one argument, ``model``. ``model`` will be an instance of a child class of `Model`_, which provides an interface to the machine learning backend. You can also add a description which will display on the landing page. +Some explanation for the ``FunViz`` class in ``fun_viz.py``: All visualizations should inherit from :class:`~picasso.visualizations.base.__init__.BaseVisualization`. You can also add a description which will display on the landing page. Some explanation for ``FunViz.html``: The web app is uses `Flask`_, which uses `Jinja2`_ templating. This explains the funny ``{% %}`` delimiters. The ``{% extends "result.html" %}`` just tells the your page to inherit from a boilerplate. All your html should sit within the ``vis`` block. @@ -53,16 +51,14 @@ Add visualization logic Our visualization should actually do something. It's just going to compute the class probabilities and pass them back along to the web app. So we'll add: .. code-block:: python3 - :emphasize-lines: 11-21 + :emphasize-lines: 9-21 - from picasso.visualizations import BaseVisualization + from picasso.visualizations.base import BaseVisualization class FunViz(BaseVisualization): - def __init__(self, model): - self.description = 'A fun visualization!' - self.model = model + DESCRIPTION = 'A fun visualization!' def make_visualization(self, inputs, output_dir, settings=None): pre_processed_arrays = self.model.preprocess([example['data'] @@ -311,20 +307,19 @@ Similarly, there is an ``outputs/`` folder (not shown in this example). Its pat Add some settings ================= -Maybe we'd like the user to be able to limit the number of classes shown. We can easily do this by adding a ``settings`` property to the ``FunViz`` class. +Maybe we'd like the user to be able to limit the number of classes shown. We can easily do this by adding an ``ALLOWED_SETTINGS`` property to the ``FunViz`` class. .. code-block:: python3 - :emphasize-lines: 5, 21 + :emphasize-lines: 6, 20 from picasso.visualizations import BaseVisualization class FunViz(BaseVisualization): - settings = {'Display': ['1', '2', '3']} - def __init__(self, model): - self.description = 'A fun visualization!' - self.model = model + ALLOWED_SETTINGS = {'Display': ['1', '2', '3']} + + DESCRIPTION = 'A fun visualization!' def make_visualization(self, inputs, output_dir, settings=None): pre_processed_arrays = self.model.preprocess([example['data'] @@ -391,10 +386,6 @@ For more complex visualizations, see the examples in `the visualizations module` .. _template: https://github.com/merantix/picasso/blob/master/picasso/templates/ClassProbabilities.html -.. _BaseVisualization: https://github.com/merantix/picasso/blob/master/picasso/visualizations/__init__.py - -.. _Model: https://github.com/merantix/picasso/blob/master/picasso/ml_frameworks/model.py - .. _Flask: http://flask.pocoo.org/ .. _Jinja2: http://jinja.pocoo.org/docs/ From 33cf81ef5427911e058e98fc12971aecc12b8d23 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 7 Jul 2017 10:52:40 +0200 Subject: [PATCH 18/18] warning message for deprecated settings --- picasso/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/picasso/__init__.py b/picasso/__init__.py index 910a03e..9d21010 100644 --- a/picasso/__init__.py +++ b/picasso/__init__.py @@ -23,13 +23,18 @@ 'BACKEND_POSTPROCESSOR_NAME', 'BACKEND_POSTPROCESSOR_PATH', 'BACKEND_PROB_DECODER_NAME', - 'BACKEND_PROB_DECODER_PATH'] + 'BACKEND_PROB_DECODER_PATH', + 'DATA_DIR'] if any([x in app.config.keys() for x in deprecated_settings]): raise ValueError('It looks like you\'re using a deprecated' ' setting. The settings and utility functions' ' have been changed as of version v0.2.0 (and ' 'you\'re using {}). Changing to the updated ' - ' settings is trivial: see xxxxxxx.'.format(__version__)) + ' settings is trivial: see ' + 'https://picasso.readthedocs.io/en/latest/models.html' + ' and ' + 'https://picasso.readthedocs.io/en/latest/settings.html' + .format(__version__)) import picasso.picasso