Skip to content

Commit 1d396da

Browse files
authored
Class config (#21)
* move configuration to classes * Code style and formatting changes. * update documentation
1 parent 4878f96 commit 1d396da

32 files changed

+785
-875
lines changed

docs/models.rst

Lines changed: 41 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,18 @@ We include three `examples`_ for you to try: a model trained on the `MNIST`_ dat
1212

1313
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 [#]_.
1414

15-
You can specify the backend (Tensorflow or Keras) using the ``PICASSO_BACKEND_ML`` setting. The allowed values are ``tensorflow`` or ``keras`` (see :doc:`settings`).
16-
1715
Your model data
1816
===============
1917

20-
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`_).
18+
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`_).
2119

2220
Utility functions
2321
=================
2422

2523
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`.
2624

27-
We'll draw from the `Keras MNIST example`_ for this guide.
25+
We'll draw from the `Keras MNIST example`_ for this guide. All custom models
26+
from the relevant model: either ``KerasModel`` or ``TensorflowModel``.
2827

2928
Preprocessor
3029
------------
@@ -33,39 +32,29 @@ The preprocessor takes images uploaded to the webapp and converts them into arra
3332

3433
.. code-block:: python3
3534
36-
MNIST_DIM = (28, 28)
37-
38-
def preprocess(targets):
39-
image_arrays = []
40-
for target in targets:
41-
im = target.convert('L')
42-
im = im.resize(MNIST_DIM, Image.ANTIALIAS)
43-
arr = np.array(im)
44-
image_arrays.append(arr)
45-
46-
all_targets = np.array(image_arrays)
47-
return all_targets.reshape(len(all_targets),
48-
MNIST_DIM[0],
49-
MNIST_DIM[1], 1).astype('float32') / 255
50-
51-
Specifically, we have to convert an arbitrary input color image to a float array of the input size specified with ``MNIST_DIM``.
35+
import numpy as np
36+
from PIL import Image
37+
38+
from picasso.models.keras import KerasModel
5239
53-
Postprocessor
54-
-------------
40+
MNIST_DIM = (28, 28)
5541
56-
For some visualizations, it's useful to convert a flat representation back into an array with the same shape as the original image.
42+
class KerasMNISTModel(KerasModel):
5743
58-
.. code-block:: python3
44+
def preprocess(self, raw_inputs):
45+
image_arrays = []
46+
for target in targets:
47+
im = target.convert('L')
48+
im = im.resize(MNIST_DIM, Image.ANTIALIAS)
49+
arr = np.array(im)
50+
image_arrays.append(arr)
5951
60-
def postprocess(output_arr):
61-
images = []
62-
for row in output_arr:
63-
im_array = row.reshape(MNIST_DIM)
64-
images.append(im_array)
52+
all_targets = np.array(image_arrays)
53+
return all_targets.reshape(len(all_targets),
54+
MNIST_DIM[0],
55+
MNIST_DIM[1], 1).astype('float32') / 255
6556
66-
return images
67-
68-
This therefore takes an arbitrary array (with the same number of total entries as the image array) and reshapes it back.
57+
Specifically, we have to convert an arbitrary input color image to a float array of the input size specified with ``MNIST_DIM``.
6958

7059
Class Decoder
7160
-------------
@@ -74,24 +63,27 @@ Class probabilities are usually returned in an array. For any visualization whe
7463

7564
.. code-block:: python3
7665
77-
def prob_decode(probability_array, top=5):
78-
results = []
79-
for row in probability_array:
80-
entries = []
81-
for i, prob in enumerate(row):
82-
entries.append({'index': i,
83-
'name': str(i),
84-
'prob': prob})
66+
class KerasMNISTModel(KerasModel):
8567
86-
entries = sorted(entries,
87-
key=itemgetter('prob'),
88-
reverse=True)[:top]
68+
...
69+
70+
def decode_prob(self, class_probabilities):
71+
results = []
72+
for row in class_probabilities:
73+
entries = []
74+
for i, prob in enumerate(row):
75+
entries.append({'index': i,
76+
'name': str(i),
77+
'prob': prob})
8978
90-
for entry in entries:
91-
entry['prob'] = '{:.3f}'.format(entry['prob'])
92-
results.append(entries)
79+
entries = sorted(entries,
80+
key=itemgetter('prob'),
81+
reverse=True)[:self.top_probs]
9382
94-
return results
83+
for entry in entries:
84+
entry['prob'] = '{:.3f}'.format(entry['prob'])
85+
results.append(entries)
86+
return results
9587
9688
``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).
9789

@@ -103,9 +95,9 @@ Class probabilities are usually returned in an array. For any visualization whe
10395

10496
.. _Sequential: https://keras.io/models/sequential/
10597

106-
.. _Keras model code: https://github.com/merantix/picasso/blob/master/picasso/ml_frameworks/keras/model.py
98+
.. _Keras model code: https://github.com/merantix/picasso/blob/master/picasso/keras/keras.py
10799

108-
.. _Keras MNIST example: https://github.com/merantix/picasso/blob/master/picasso/examples/keras/util.py
100+
.. _Keras MNIST example: https://github.com/merantix/picasso/blob/master/picasso/examples/keras/model.py
109101

110102
.. _PIL Image: http://pillow.readthedocs.io/en/latest/reference/Image.html
111103

docs/settings.rst

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,18 @@ Tells the app to use this configuration instead of the default one. Inside
2121
2222
base_dir = os.path.split(os.path.abspath(__file__))[0]
2323
24-
BACKEND_ML = 'tensorflow'
25-
BACKEND_PREPROCESSOR_NAME = 'util'
26-
BACKEND_PREPROCESSOR_PATH = os.path.join(base_dir, 'util.py')
27-
BACKEND_POSTPROCESSOR_NAME = 'postprocess'
28-
BACKEND_POSTPROCESSOR_PATH = os.path.join(base_dir, 'util.py')
29-
BACKEND_PROB_DECODER_NAME = 'prob_decode'
30-
BACKEND_PROB_DECODER_PATH = os.path.join(base_dir, 'util.py')
31-
DATA_DIR = os.path.join(base_dir, 'data-volume')
32-
33-
Any lowercase line is ignored for the purposes of determining a setting. These
34-
can also be set via environment variables, but you must append the app name.
35-
For instance ``BACKEND_ML = 'tensorflow'`` would become ``export
36-
PICASSO_BACKEND_ML=tensorflow``.
37-
38-
For explanations of each setting, see :mod:`picasso.settings`. Any
39-
additional settings starting with `BACKEND_` will be sent to the model backend
40-
as a keyword argument. The input and output tensor names can be passed to the
41-
Tensorflow backend in this way:
42-
43-
.. code-block:: python3
44-
45-
...
46-
BACKEND_TF_PREDICT_VAR='Softmax:0'
47-
BACKEND_TF_INPUT_VAR='convolution2d_input_1:0'
24+
MODEL_CLS_PATH = os.path.join(base_dir, 'model.py')
25+
MODEL_CLS_NAME = 'TensorflowMNISTModel'
26+
MODEL_LOAD_ARGS = {
27+
'data_dir': os.path.join(base_dir, 'data-volume'),
28+
'tf_input_var': 'convolution2d_input_1:0',
29+
'tf_predict_var': 'Softmax:0',
30+
}
31+
32+
Any lowercase line is ignored for the purposes of determining a setting.
33+
``MODEL_LOAD_ARGS`` will pass the arguments along to the model's ``load``
34+
function.
35+
36+
For explanations of each setting, see :mod:`picasso.config`.
4837

4938
.. _managed by Flask: http://flask.pocoo.org/docs/latest/config/

docs/visualizations.rst

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,12 @@ For our example, ``FunViz``, we'll need ``picasso/visualizations/fun_viz.py``:
1313

1414
.. code-block:: python3
1515
16-
from picasso.visualizations import BaseVisualization
16+
from picasso.visualizations.base import BaseVisualization
1717
1818
1919
class FunViz(BaseVisualization):
2020
21-
def __init__(self, model):
22-
self.description = 'A fun visualization!'
23-
self.model = model
21+
DESCRIPTION = 'A fun visualization!'
2422
2523
def make_visualization(self, inputs, output_dir, settings=None):
2624
pass
@@ -34,7 +32,7 @@ and ``picasso/templates/FunViz.html``:
3432
your visualization html goes here
3533
{% endblock %}
3634
37-
Some explanation for the ``FunViz`` class in ``fun_viz.py``: All visualizations should inherit from :class:`~picasso.visualizations.__init__.BaseVisualization` (see `code <BaseVisualization>`_). 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.
35+
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.
3836

3937
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.
4038

@@ -53,16 +51,14 @@ Add visualization logic
5351
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:
5452

5553
.. code-block:: python3
56-
:emphasize-lines: 11-21
54+
:emphasize-lines: 9-21
5755
58-
from picasso.visualizations import BaseVisualization
56+
from picasso.visualizations.base import BaseVisualization
5957
6058
6159
class FunViz(BaseVisualization):
6260
63-
def __init__(self, model):
64-
self.description = 'A fun visualization!'
65-
self.model = model
61+
DESCRIPTION = 'A fun visualization!'
6662
6763
def make_visualization(self, inputs, output_dir, settings=None):
6864
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
311307
Add some settings
312308
=================
313309

314-
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.
310+
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.
315311

316312
.. code-block:: python3
317-
:emphasize-lines: 5, 21
313+
:emphasize-lines: 6, 20
318314
319315
from picasso.visualizations import BaseVisualization
320316
321317
322318
class FunViz(BaseVisualization):
323-
settings = {'Display': ['1', '2', '3']}
324319
325-
def __init__(self, model):
326-
self.description = 'A fun visualization!'
327-
self.model = model
320+
ALLOWED_SETTINGS = {'Display': ['1', '2', '3']}
321+
322+
DESCRIPTION = 'A fun visualization!'
328323
329324
def make_visualization(self, inputs, output_dir, settings=None):
330325
pre_processed_arrays = self.model.preprocess([example['data']
@@ -391,10 +386,6 @@ For more complex visualizations, see the examples in `the visualizations module`
391386

392387
.. _template: https://github.com/merantix/picasso/blob/master/picasso/templates/ClassProbabilities.html
393388

394-
.. _BaseVisualization: https://github.com/merantix/picasso/blob/master/picasso/visualizations/__init__.py
395-
396-
.. _Model: https://github.com/merantix/picasso/blob/master/picasso/ml_frameworks/model.py
397-
398389
.. _Flask: http://flask.pocoo.org/
399390

400391
.. _Jinja2: http://jinja.pocoo.org/docs/

picasso/__init__.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,28 @@
1313
raise SystemError('Python 3.5+ required, found {}'.format(sys.version))
1414

1515
app = Flask(__name__)
16-
app.config.from_object('picasso.settings.Default')
16+
app.config.from_object('picasso.config.Default')
1717

1818
if os.getenv('PICASSO_SETTINGS'):
1919
app.config.from_envvar('PICASSO_SETTINGS')
2020

21+
deprecated_settings = ['BACKEND_PREPROCESSOR_NAME',
22+
'BACKEND_PREPROCESSOR_PATH',
23+
'BACKEND_POSTPROCESSOR_NAME',
24+
'BACKEND_POSTPROCESSOR_PATH',
25+
'BACKEND_PROB_DECODER_NAME',
26+
'BACKEND_PROB_DECODER_PATH',
27+
'DATA_DIR']
28+
29+
if any([x in app.config.keys() for x in deprecated_settings]):
30+
raise ValueError('It looks like you\'re using a deprecated'
31+
' setting. The settings and utility functions'
32+
' have been changed as of version v0.2.0 (and '
33+
'you\'re using {}). Changing to the updated '
34+
' settings is trivial: see '
35+
'https://picasso.readthedocs.io/en/latest/models.html'
36+
' and '
37+
'https://picasso.readthedocs.io/en/latest/settings.html'
38+
.format(__version__))
39+
2140
import picasso.picasso

picasso/config.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import os
2+
3+
base_dir = os.path.dirname(__file__) # only for default config
4+
5+
6+
class Default:
7+
"""Default settings for the Flask app.
8+
9+
The Flask app uses these settings if no custom settings are defined. You
10+
can define custom settings by creating a Python module, defining global
11+
variables in that module, and setting the environment variable
12+
`PICASSO_SETTINGS` to the path to that module.
13+
14+
If `PICASSO_SETTINGS` is not set, or if any particular setting is not
15+
defined in the indicated module, then the Flask app uses these default
16+
settings.
17+
18+
"""
19+
# :obj:`str`: filepath of the module containing the model to run
20+
MODEL_CLS_PATH = os.path.join(
21+
base_dir, 'examples', 'keras', 'model.py')
22+
23+
# :obj:`str`: name of model class
24+
MODEL_CLS_NAME = 'KerasMNISTModel'
25+
26+
# :obj:`dict`: dictionary of args to pass to the `load` method of the
27+
# model instance.
28+
MODEL_LOAD_ARGS = {
29+
'data_dir': os.path.join(base_dir, 'examples', 'keras', 'data-volume'),
30+
}
Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
# Note: By default, Flask doesn't know that this file exists. If you want
2+
# Flask to load the settings you specify here, you must set the environment
3+
# variable `PICASSO_SETTINGS` to point to this file. E.g.:
4+
#
5+
# export PICASSO_SETTINGS=/path/to/examples/keras-vgg16/config.py
6+
#
17
import os
28

39
base_dir = os.path.dirname(os.path.abspath(__file__))
410

5-
BACKEND_ML = 'keras'
6-
BACKEND_PREPROCESSOR_NAME = 'preprocess'
7-
BACKEND_PREPROCESSOR_PATH = os.path.join(base_dir, 'util.py')
8-
BACKEND_POSTPROCESSOR_NAME = 'postprocess'
9-
BACKEND_POSTPROCESSOR_PATH = os.path.join(base_dir, 'util.py')
10-
BACKEND_PROB_DECODER_NAME = 'prob_decode'
11-
BACKEND_PROB_DECODER_PATH = os.path.join(base_dir, 'util.py')
12-
DATA_DIR = os.path.join(base_dir, 'data-volume')
11+
MODEL_CLS_PATH = os.path.join(base_dir, 'model.py')
12+
MODEL_CLS_NAME = 'KerasVGG16Model'
13+
MODEL_LOAD_ARGS = {
14+
'data_dir': os.path.join(base_dir, 'data-volume'),
15+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from keras.applications import imagenet_utils
2+
import numpy as np
3+
from PIL import Image
4+
5+
from picasso.models.keras import KerasModel
6+
7+
VGG16_DIM = (224, 224, 3)
8+
9+
10+
class KerasVGG16Model(KerasModel):
11+
12+
def preprocess(self, raw_inputs):
13+
"""
14+
Args:
15+
raw_inputs (list of Images): a list of PIL Image objects
16+
Returns:
17+
array (float32): num images * height * width * num channels
18+
"""
19+
image_arrays = []
20+
for raw_im in raw_inputs:
21+
im = raw_im.resize(VGG16_DIM[:2], Image.ANTIALIAS)
22+
im = im.convert('RGB')
23+
arr = np.array(im).astype('float32')
24+
image_arrays.append(arr)
25+
26+
all_raw_inputs = np.array(image_arrays)
27+
return imagenet_utils.preprocess_input(all_raw_inputs)
28+
29+
def decode_prob(self, class_probabilities):
30+
r = imagenet_utils.decode_predictions(class_probabilities,
31+
top=self.top_probs)
32+
results = [
33+
[{'code': entry[0],
34+
'name': entry[1],
35+
'prob': '{:.3f}'.format(entry[2])}
36+
for entry in row]
37+
for row in r
38+
]
39+
classes = imagenet_utils.CLASS_INDEX
40+
class_keys = list(classes.keys())
41+
class_values = list(classes.values())
42+
43+
for result in results:
44+
for entry in result:
45+
entry['index'] = int(
46+
class_keys[class_values.index([entry['code'],
47+
entry['name']])])
48+
return results

0 commit comments

Comments
 (0)