Implemented extensive support for neural networks, images and directories [closes #121]

This commit is contained in:
Fabio Manganiello 2020-03-23 01:00:25 +01:00
parent 1f1fefca9d
commit 50e372be36
8 changed files with 289 additions and 75 deletions

View file

@ -48,6 +48,7 @@ Events
platypush/events/serial.rst platypush/events/serial.rst
platypush/events/sound.rst platypush/events/sound.rst
platypush/events/stt.rst platypush/events/stt.rst
platypush/events/tensorflow.rst
platypush/events/todoist.rst platypush/events/todoist.rst
platypush/events/torrent.rst platypush/events/torrent.rst
platypush/events/travisci.rst platypush/events/travisci.rst

View file

@ -0,0 +1,5 @@
``platypush.message.event.tensorflow``
======================================
.. automodule:: platypush.message.event.tensorflow
:members:

View file

@ -0,0 +1,5 @@
``platypush.plugins.tensorflow``
================================
.. automodule:: platypush.plugins.tensorflow
:members:

View file

@ -0,0 +1,5 @@
``platypush.message.response.tensorflow``
=========================================
.. automodule:: platypush.message.response.tensorflow
:members:

View file

@ -104,6 +104,7 @@ Plugins
platypush/plugins/switch.wemo.rst platypush/plugins/switch.wemo.rst
platypush/plugins/system.rst platypush/plugins/system.rst
platypush/plugins/tcp.rst platypush/plugins/tcp.rst
platypush/plugins/tensorflow.rst
platypush/plugins/todoist.rst platypush/plugins/todoist.rst
platypush/plugins/torrent.rst platypush/plugins/torrent.rst
platypush/plugins/travisci.rst platypush/plugins/travisci.rst

View file

@ -18,6 +18,7 @@ Responses
platypush/responses/qrcode.rst platypush/responses/qrcode.rst
platypush/responses/stt.rst platypush/responses/stt.rst
platypush/responses/system.rst platypush/responses/system.rst
platypush/responses/tensorflow.rst
platypush/responses/todoist.rst platypush/responses/todoist.rst
platypush/responses/trello.rst platypush/responses/trello.rst
platypush/responses/weather.buienradar.rst platypush/responses/weather.buienradar.rst

View file

@ -1,4 +1,7 @@
from typing import Dict, List, Union from typing import Dict, List, Union, Optional
import numpy as np
from tensorflow.keras.models import Model
from platypush.message.response import Response from platypush.message.response import Response
@ -7,14 +10,16 @@ class TensorflowResponse(Response):
""" """
Generic Tensorflow response. Generic Tensorflow response.
""" """
def __init__(self, *args, model: str, **kwargs): def __init__(self, *args, model: Model, **kwargs):
""" """
:param model: Name of the model. :param model: Name of the model.
""" """
super().__init__(*args, output={ super().__init__(*args, output={
'model': model, 'model': model.name,
}, **kwargs) }, **kwargs)
self.model = model
class TensorflowTrainResponse(TensorflowResponse): class TensorflowTrainResponse(TensorflowResponse):
""" """
@ -31,4 +36,27 @@ class TensorflowTrainResponse(TensorflowResponse):
self.output['history'] = history self.output['history'] = history
class TensorflowPredictResponse(TensorflowResponse):
"""
Tensorflow model prediction response.
"""
def __init__(self, *args, prediction: np.ndarray, output_labels: Optional[List[str]] = None, **kwargs):
super().__init__(*args, **kwargs)
if output_labels and len(output_labels) == self.model.outputs[-1].shape[-1]:
self.output['values'] = [
{output_labels[i]: value for i, value in enumerate(p)}
for p in prediction
]
else:
self.output['values'] = prediction
if self.model.__class__.__name__ != 'LinearModel':
prediction = [int(np.argmax(p)) for p in prediction]
if output_labels:
self.output['labels'] = [output_labels[p] for p in prediction]
else:
self.output['labels'] = prediction
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -1,18 +1,23 @@
import json
import os import os
import random
import shutil import shutil
import threading import threading
from datetime import datetime
from typing import List, Dict, Any, Union, Optional, Tuple, Iterable from typing import List, Dict, Any, Union, Optional, Tuple, Iterable
import numpy as np import numpy as np
from tensorflow.keras import Model from tensorflow.keras import Model
from tensorflow.keras.layers import Layer from tensorflow.keras.layers import Layer
from tensorflow.keras.models import load_model from tensorflow.keras.models import load_model
from tensorflow.keras.preprocessing import image
from tensorflow.keras import utils
from platypush.config import Config from platypush.config import Config
from platypush.context import get_bus from platypush.context import get_bus
from platypush.message.event.tensorflow import TensorflowEpochStartedEvent, TensorflowEpochEndedEvent, \ from platypush.message.event.tensorflow import TensorflowEpochStartedEvent, TensorflowEpochEndedEvent, \
TensorflowBatchStartedEvent, TensorflowBatchEndedEvent, TensorflowTrainStartedEvent, TensorflowTrainEndedEvent TensorflowBatchStartedEvent, TensorflowBatchEndedEvent, TensorflowTrainStartedEvent, TensorflowTrainEndedEvent
from platypush.message.response.tensorflow import TensorflowTrainResponse from platypush.message.response.tensorflow import TensorflowTrainResponse, TensorflowPredictResponse
from platypush.plugins import Plugin, action from platypush.plugins import Plugin, action
@ -45,7 +50,10 @@ class TensorflowPlugin(Plugin):
""" """
_supported_data_file_extensions = ['npy', 'npz', 'csv'] _image_extensions = ['jpg', 'jpeg', 'bmp', 'tiff', 'tif', 'png', 'gif']
_numpy_extensions = ['npy', 'npz']
_csv_extensions = ['csv', 'tsv']
_supported_data_file_extensions = [*_csv_extensions, *_numpy_extensions, *_image_extensions]
def __init__(self, workdir: str = os.path.join(Config.get('workdir'), 'tensorflow'), **kwargs): def __init__(self, workdir: str = os.path.join(Config.get('workdir'), 'tensorflow'), **kwargs):
""" """
@ -65,7 +73,20 @@ class TensorflowPlugin(Plugin):
model_dir = os.path.join(self._models_dir, name) model_dir = os.path.join(self._models_dir, name)
assert os.path.isdir(model_dir), 'The model {} does not exist'.format(name) assert os.path.isdir(model_dir), 'The model {} does not exist'.format(name)
return load_model(model_dir) model = load_model(model_dir)
model.input_labels = []
model.output_labels = []
labels_file = os.path.join(model_dir, 'labels.json')
if os.path.isfile(labels_file):
with open(labels_file, 'r') as f:
labels = json.load(f)
if 'input' in labels:
model.input_labels = labels['input']
if 'output' in labels:
model.output_labels = labels['output']
return model
def _generate_callbacks(self, model: str): def _generate_callbacks(self, model: str):
from tensorflow.keras.callbacks import LambdaCallback from tensorflow.keras.callbacks import LambdaCallback
@ -115,40 +136,40 @@ class TensorflowPlugin(Plugin):
return callback return callback
@action @action
def load(self, name: str, reload: bool = False) -> Dict[str, Any]: def load(self, model: str, reload: bool = False) -> Dict[str, Any]:
""" """
(Re)-load a model from the file system. (Re)-load a model from the file system.
:param name: Name of the model. Must be a folder name stored under ``<workdir>/models``. :param model: Name of the model. Must be a folder name stored under ``<workdir>/models``.
:param reload: If ``True``, the model will be reloaded from the filesystem even if it's been already :param reload: If ``True``, the model will be reloaded from the filesystem even if it's been already
loaded, otherwise the model currently in memory will be kept (default: ``False``). loaded, otherwise the model currently in memory will be kept (default: ``False``).
:return: The model configuration. :return: The model configuration.
""" """
model = self._load_model(name, reload=reload) model = self._load_model(model, reload=reload)
return model.get_config() return model.get_config()
@action @action
def unload(self, name: str) -> None: def unload(self, model: str) -> None:
""" """
Remove a loaded model from memory. Remove a loaded model from memory.
:param name: Name of the model. :param model: Name of the model.
""" """
assert name in self.models, 'The model {} is not loaded'.format(name) assert model in self.models, 'The model {} is not loaded'.format(model)
del self.models[name] del self.models[model]
@action @action
def remove(self, name: str) -> None: def remove(self, model: str) -> None:
""" """
Unload a module and, if stored on the filesystem, remove its resource files as well. Unload a module and, if stored on the filesystem, remove its resource files as well.
WARNING: This operation is not reversible. WARNING: This operation is not reversible.
:param name: Name of the model. :param model: Name of the model.
""" """
if name in self.models: if model in self.models:
del self.models[name] del self.models[model]
model_dir = os.path.join(self._models_dir, name) model_dir = os.path.join(self._models_dir, model)
if os.path.isdir(model_dir): if os.path.isdir(model_dir):
shutil.rmtree(model_dir) shutil.rmtree(model_dir)
@ -205,7 +226,7 @@ class TensorflowPlugin(Plugin):
] ]
:param input_names: List of names for the input units (default: TensorFlow name auto-assign logic). :param input_names: List of names for the input units (default: TensorFlow name auto-assign logic).
:param output_names: List of names for the output units (default: TensorFlow name auto-assign logic). :param output_names: List of labels for the output units (default: TensorFlow name auto-assign logic).
:param optimizer: Optimizer, see <https://keras.io/optimizers/> (default: ``rmsprop``). :param optimizer: Optimizer, see <https://keras.io/optimizers/> (default: ``rmsprop``).
:param loss: Loss function, see <https://keras.io/losses/>. An objective function is any callable with :param loss: Loss function, see <https://keras.io/losses/>. An objective function is any callable with
the signature ``scalar_loss = fn(y_true, y_pred)``. If the model has multiple outputs, you can use a the signature ``scalar_loss = fn(y_true, y_pred)``. If the model has multiple outputs, you can use a
@ -360,11 +381,8 @@ class TensorflowPlugin(Plugin):
**kwargs **kwargs
) )
if input_names: model.input_labels = input_names or []
model.input_names = input_names model.output_labels = output_names or []
if output_names:
model.output_names = output_names
self.models[name] = model self.models[name] = model
return model.get_config() return model.get_config()
@ -395,7 +413,7 @@ class TensorflowPlugin(Plugin):
:param name: Name of the model. :param name: Name of the model.
:param units: Output dimension (default: 1). :param units: Output dimension (default: 1).
:param input_names: List of names for the input units (default: TensorFlow name auto-assign logic). :param input_names: List of names for the input units (default: TensorFlow name auto-assign logic).
:param output_names: List of names for the output units (default: TensorFlow name auto-assign logic). :param output_names: List of labels for the output units (default: TensorFlow name auto-assign logic).
:param activation: Activation function to be used (default: None). :param activation: Activation function to be used (default: None).
:param use_bias: Whether to calculate the bias/intercept for this model. If set :param use_bias: Whether to calculate the bias/intercept for this model. If set
to False, no bias/intercept will be used in calculations, e.g., the data to False, no bias/intercept will be used in calculations, e.g., the data
@ -475,11 +493,13 @@ class TensorflowPlugin(Plugin):
bias_regularizer=bias_regularizer, bias_regularizer=bias_regularizer,
name=name) name=name)
if input_names: model.input_names = input_names or []
model.input_names = input_names
if output_names: if output_names:
assert units == len(output_names) assert units == len(output_names)
model.output_names = output_names model.output_labels = output_names
else:
model.output_labels = []
model.compile( model.compile(
optimizer=optimizer, optimizer=optimizer,
@ -516,31 +536,106 @@ class TensorflowPlugin(Plugin):
return list(np.load(data_file).values()).pop() return list(np.load(data_file).values()).pop()
@classmethod @classmethod
def _get_data(cls, data: Union[str, np.ndarray, Iterable, Dict[str, Union[Iterable, np.ndarray]]]) \ def _get_image(cls, image_file: str, model: Model) -> np.ndarray:
input_shape = model.inputs[0].shape
size = input_shape[1:3].as_list()
assert len(size) == 2, 'The model {} does not have enough dimensions to process an image (shape: {})'.format(
model.name, size)
colors = input_shape[3:]
assert colors, ('The model {} requires a tensor with at least 3 inputs in order to process images: ' +
'[WIDTH, HEIGHT, COLORS]').format(model.name)
if colors[0] == 1:
color_mode = 'grayscale'
elif colors[0] == 3:
color_mode = 'rgb'
elif colors[0] == 4:
color_mode = 'rgba'
else:
raise AssertionError('The input tensor should have either 1 (grayscale), 3 (rgb) or 4 (rgba) units. ' +
'Found: {}'.format(colors[0]))
img = image.load_img(image_file, target_size=size, color_mode=color_mode)
return image.img_to_array(img)
@classmethod
def _get_dir(cls, directory: str, model: Model) -> Dict[str, Iterable]:
labels = [f for f in os.listdir(directory) if os.path.isdir(os.path.join(directory, f))]
assert set(model.output_labels) == set(labels),\
'The directory {dir} should contain exactly {n} subfolders named {names}'.format(
dir=directory, n=len(model.output_labels), names=model.output.labels)
ret = {}
for label in labels:
subdir = os.path.join(directory, label)
ret[label] = [
cls._get_data(os.path.join(subdir, f), model)
for f in os.listdir(subdir)
if f.split('.')[-1] in cls._supported_data_file_extensions
]
return ret
@classmethod
def _get_data(cls, data: Union[str, np.ndarray, Iterable, Dict[str, Union[Iterable, np.ndarray]]], model: Model) \
-> Union[np.ndarray, Iterable, Dict[str, Union[Iterable, np.ndarray]]]: -> Union[np.ndarray, Iterable, Dict[str, Union[Iterable, np.ndarray]]]:
if not isinstance(data, str): if not isinstance(data, str):
return data return data
data_file = os.path.abspath(os.path.expanduser(data)) if data.startswith('http://') or data.startswith('https://'):
filename = '{timestamp}_{filename}'.format(
timestamp=datetime.now().timestamp(), filename=data.split('/')[-1])
data_file = utils.get_file(filename, data)
else:
data_file = os.path.abspath(os.path.expanduser(data))
extensions = [ext for ext in cls._supported_data_file_extensions if data_file.endswith('.' + ext)] extensions = [ext for ext in cls._supported_data_file_extensions if data_file.endswith('.' + ext)]
assert os.path.isfile(data_file)
assert extensions, 'Unsupported type for file {}. Supported extensions: {}'.format(
data_file, cls._supported_data_file_extensions
)
extension = extensions.pop() if os.path.isfile(data_file):
if extension == 'csv': assert extensions, 'Unsupported type for file {}. Supported extensions: {}'.format(
return cls._get_csv_data(data_file) data_file, cls._supported_data_file_extensions
if extension == 'npy': )
return cls._get_numpy_data(data_file)
if extension == 'npz':
return cls._get_numpy_compressed_data(data_file)
raise AssertionError('Something went wrong while loading the data file {}'.format(data_file)) extension = extensions.pop()
if extension in cls._csv_extensions:
return cls._get_csv_data(data_file)
if extension == 'npy':
return cls._get_numpy_data(data_file)
if extension == 'npz':
return cls._get_numpy_compressed_data(data_file)
if extension in cls._image_extensions:
return cls._get_image(data_file, model)
raise AssertionError('Unsupported file type: {}'.format(data_file))
elif os.path.isdir(data_file):
return cls._get_dir(data_file, model)
@classmethod
def _get_dataset(cls,
inputs: Union[str, np.ndarray, Iterable, Dict[str, Union[Iterable, np.ndarray]]],
outputs: Optional[Union[str, np.ndarray, Iterable, Dict[str, Union[Iterable, np.ndarray]]]],
model: Model) \
-> Tuple[Union[np.ndarray, Iterable, Dict[str, Union[Iterable, np.ndarray]]],
Optional[Union[np.ndarray, Iterable, Dict[str, Union[Iterable, np.ndarray]]]]]:
inputs = cls._get_data(inputs, model)
if outputs:
outputs = cls._get_data(inputs, model)
elif isinstance(inputs, dict) and model.output_labels:
pairs = []
for i, label in enumerate(model.output_labels):
data = inputs.get(label, [])
pairs.extend([(d, i) for d in data])
random.shuffle(pairs)
inputs = np.asarray([p[0] for p in pairs])
outputs = np.asarray([p[1] for p in pairs])
return inputs, outputs
@action @action
def train(self, def train(self,
name: str, model: str,
inputs: Union[str, np.ndarray, Iterable, Dict[str, Union[Iterable, np.ndarray]]], inputs: Union[str, np.ndarray, Iterable, Dict[str, Union[Iterable, np.ndarray]]],
outputs: Optional[Union[str, np.ndarray, Iterable]] = None, outputs: Optional[Union[str, np.ndarray, Iterable]] = None,
batch_size: Optional[int] = None, batch_size: Optional[int] = None,
@ -561,7 +656,7 @@ class TensorflowPlugin(Plugin):
""" """
Trains a model on a dataset for a fixed number of epochs. Trains a model on a dataset for a fixed number of epochs.
:param name: Name of the existing model to be trained. :param model: Name of the existing model to be trained.
:param inputs: Input data. It can be: :param inputs: Input data. It can be:
- A numpy array (or array-like), or a list of arrays in case the model has multiple inputs. - A numpy array (or array-like), or a list of arrays in case the model has multiple inputs.
@ -575,9 +670,19 @@ class TensorflowPlugin(Plugin):
- CSV with header (``.csv`` extension``) - CSV with header (``.csv`` extension``)
- Numpy raw or compressed files (``.npy`` or ``.npz`` extension) - Numpy raw or compressed files (``.npy`` or ``.npz`` extension)
- Image files
- An HTTP URL pointing to one of the file types listed above
- Directories with images. If ``inputs`` points to a directory of images then the following
conventions are followed:
:param outputs: Target data. Like the input data `x`, it can be a numpy array (or array-like) or TensorFlow tensor(s). - The folder must contain exactly as many subfolders as the output units of your model. If
It should be consistent with `x` (you cannot have Numpy inputs and tensor targets, or inversely). the model has ``output_labels`` then those subfolders should be named as the output labels.
Each subfolder will contain training examples that match the associated label (e.g.
``positive`` will contain all the positive images and ``negative`` all the negative images).
- ``outputs`` doesn't have to be specified.
:param outputs: Target data. Like the input data `x`, it can be a numpy array (or array-like) or TensorFlow
tensor(s). It should be consistent with `x` (you cannot have Numpy inputs and tensor targets, or inversely).
If `x` is a dataset, generator, or `keras.utils.Sequence` instance, `y` should not be specified If `x` is a dataset, generator, or `keras.utils.Sequence` instance, `y` should not be specified
(since targets will be obtained from `x`). (since targets will be obtained from `x`).
@ -664,10 +769,9 @@ class TensorflowPlugin(Plugin):
:return: :class:`platypush.message.response.tensorflow.TensorflowTrainResponse` :return: :class:`platypush.message.response.tensorflow.TensorflowTrainResponse`
""" """
model = self._load_model(name) name = model
inputs = self._get_data(inputs) model = self._load_model(model)
if outputs: inputs, outputs = self._get_dataset(inputs, outputs, model)
outputs = self._get_data(outputs)
ret = model.fit( ret = model.fit(
x=inputs, x=inputs,
@ -690,11 +794,11 @@ class TensorflowPlugin(Plugin):
use_multiprocessing=use_multiprocessing, use_multiprocessing=use_multiprocessing,
) )
return TensorflowTrainResponse(model=name, epochs=ret.epoch, history=ret.history) return TensorflowTrainResponse(model=model, epochs=ret.epoch, history=ret.history)
@action @action
def evaluate(self, def evaluate(self,
name: str, model: str,
inputs: Union[str, np.ndarray, Iterable, Dict[str, Union[Iterable, np.ndarray]]], inputs: Union[str, np.ndarray, Iterable, Dict[str, Union[Iterable, np.ndarray]]],
outputs: Optional[Union[str, np.ndarray, Iterable]] = None, outputs: Optional[Union[str, np.ndarray, Iterable]] = None,
batch_size: Optional[int] = None, batch_size: Optional[int] = None,
@ -707,7 +811,7 @@ class TensorflowPlugin(Plugin):
""" """
Returns the loss value and metrics values for the model in test model. Returns the loss value and metrics values for the model in test model.
:param name: Name of the existing model to be trained. :param model: Name of the existing model to be trained.
:param inputs: Input data. It can be: :param inputs: Input data. It can be:
- A numpy array (or array-like), or a list of arrays in case the model has multiple inputs. - A numpy array (or array-like), or a list of arrays in case the model has multiple inputs.
@ -721,6 +825,16 @@ class TensorflowPlugin(Plugin):
- CSV with header (``.csv`` extension``) - CSV with header (``.csv`` extension``)
- Numpy raw or compressed files (``.npy`` or ``.npz`` extension) - Numpy raw or compressed files (``.npy`` or ``.npz`` extension)
- Image files
- An HTTP URL pointing to one of the file types listed above
- Directories with images. If ``inputs`` points to a directory of images then the following
conventions are followed:
- The folder must contain exactly as many subfolders as the output units of your model. If
the model has ``output_labels`` then those subfolders should be named as the output labels.
Each subfolder will contain training examples that match the associated label (e.g.
``positive`` will contain all the positive images and ``negative`` all the negative images).
- ``outputs`` doesn't have to be specified.
:param outputs: Target data. Like the input data `x`, it can be a numpy array (or array-like) or TensorFlow tensor(s). :param outputs: Target data. Like the input data `x`, it can be a numpy array (or array-like) or TensorFlow tensor(s).
@ -765,10 +879,9 @@ class TensorflowPlugin(Plugin):
otherwise a list with the result test metrics (loss is usually the first value). otherwise a list with the result test metrics (loss is usually the first value).
""" """
model = self._load_model(name) name = model
inputs = self._get_data(inputs) model = self._load_model(model)
if outputs: inputs, outputs = self._get_dataset(inputs, outputs, model)
outputs = self._get_data(outputs)
ret = model.evaluate( ret = model.evaluate(
x=inputs, x=inputs,
@ -791,18 +904,18 @@ class TensorflowPlugin(Plugin):
@action @action
def predict(self, def predict(self,
name: str, model: str,
inputs: Union[str, np.ndarray, Iterable, Dict[str, Union[Iterable, np.ndarray]]], inputs: Union[str, np.ndarray, Iterable, Dict[str, Union[Iterable, np.ndarray]]],
batch_size: Optional[int] = None, batch_size: Optional[int] = None,
verbose: int = 0, verbose: int = 0,
steps: Optional[int] = None, steps: Optional[int] = None,
max_queue_size: int = 10, max_queue_size: int = 10,
workers: int = 1, workers: int = 1,
use_multiprocessing: bool = False) -> Union[Dict[str, float], List[float]]: use_multiprocessing: bool = False) -> TensorflowPredictResponse:
""" """
Generates output predictions for the input samples. Generates output predictions for the input samples.
:param name: Name of the existing model to be trained. :param model: Name of the existing model to be trained.
:param inputs: Input data. It can be: :param inputs: Input data. It can be:
- A numpy array (or array-like), or a list of arrays in case the model has multiple inputs. - A numpy array (or array-like), or a list of arrays in case the model has multiple inputs.
@ -816,7 +929,8 @@ class TensorflowPlugin(Plugin):
- CSV with header (``.csv`` extension``) - CSV with header (``.csv`` extension``)
- Numpy raw or compressed files (``.npy`` or ``.npz`` extension) - Numpy raw or compressed files (``.npy`` or ``.npz`` extension)
- Image files
- An HTTP URL pointing to one of the file types listed above
:param batch_size: Number of samples per gradient update. If unspecified, ``batch_size`` will default to 32. :param batch_size: Number of samples per gradient update. If unspecified, ``batch_size`` will default to 32.
Do not specify the ``batch_size`` if your data is in the form of symbolic tensors, datasets, Do not specify the ``batch_size`` if your data is in the form of symbolic tensors, datasets,
@ -840,11 +954,57 @@ class TensorflowPlugin(Plugin):
Note that because this implementation relies on multiprocessing, you should not pass non-picklable Note that because this implementation relies on multiprocessing, you should not pass non-picklable
arguments to the generator as they can't be passed easily to children processes. arguments to the generator as they can't be passed easily to children processes.
:return: ``{output_metric: metric_value}`` dictionary if the ``output_names`` of the model are specified, :return: :class:`platypush.message.response.tensorflow.TensorflowPredictResponse`. Format:
otherwise a list with the result values.
- For regression models with no output labels specified: ``values`` will contain the output vector:
.. code-block:: json
{
"values": [[3.1415]]
}
- For regression models with output labels specified: ``values`` will be a list of ``{label -> value}``
maps:
.. code-block:: json
{
"values": [
{
"x": 42.0,
"y": 43.0
}
]
}
- For neural networks: ``values`` will contain the list of the output vector like in the case of
regression, and ``labels`` will store the list of ``argmax`` (i.e. the index of the output unit with the
highest value) or their labels, if the model has output labels:
.. code-block:: json
{
"labels": [
"positive"
],
"values": [
{
"positive": 0.998,
"negative": 0.002
}
]
}
""" """
model = self._load_model(name) name = model
inputs = self._get_data(inputs) model = self._load_model(model)
inputs = self._get_data(inputs, model)
if isinstance(inputs, np.ndarray) and \
len(model.inputs[0].shape) == len(inputs.shape) + 1 and \
model.inputs[0].shape[0] is None:
inputs = np.asarray([inputs])
ret = model.predict( ret = model.predict(
inputs, inputs,
batch_size=batch_size, batch_size=batch_size,
@ -856,26 +1016,34 @@ class TensorflowPlugin(Plugin):
use_multiprocessing=use_multiprocessing use_multiprocessing=use_multiprocessing
) )
if not model.output_names: return TensorflowPredictResponse(model=model, prediction=ret, output_labels=model.output_labels)
return ret
return {model.output_names[i]: value for i, value in enumerate(ret)}
@action @action
def save(self, name: str, overwrite: bool = True, **opts) -> None: def save(self, model: str, overwrite: bool = True, **opts) -> None:
""" """
Save a model in memory to the filesystem. The model files will be stored under Save a model in memory to the filesystem. The model files will be stored under
``<WORKDIR>/models/<model_name>``. ``<WORKDIR>/models/<model_name>``.
:param name: Model name. :param model: Model name.
:param overwrite: Overwrite the model files if they already exist. :param overwrite: Overwrite the model files if they already exist.
:param opts: Extra options to be passed to ``Model.save()``. :param opts: Extra options to be passed to ``Model.save()``.
""" """
assert name in self.models, 'No such model in memory: {}'.format(name) assert model in self.models, 'No such model in memory: {}'.format(model)
model_dir = os.path.join(self._models_dir, name) model_dir = os.path.join(self._models_dir, model)
name = self.models[name] model = self.models[model]
os.makedirs(model_dir, exist_ok=True) os.makedirs(model_dir, exist_ok=True)
name.save(model_dir, overwrite=overwrite, options=opts) labels = {}
labels_file = os.path.join(model_dir, 'labels.json')
if model.input_labels:
labels['input'] = model.input_labels
if model.output_labels:
labels['output'] = model.output_labels
if labels:
with open(labels_file, 'w') as f:
json.dump(labels, f)
model.save(model_dir, overwrite=overwrite, options=opts)
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et: