Skip to content

Commit

Permalink
Merge pull request idealo#13 from bmachin/add-tfs-support
Browse files Browse the repository at this point in the history
Adding TF Serving support.
  • Loading branch information
clennan authored Nov 27, 2018
2 parents 1672c92 + 92db7e5 commit a7a01b6
Show file tree
Hide file tree
Showing 36 changed files with 629 additions and 0 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,28 @@ data/TID2013/tid2013_labels_test.json

For the AVA dataset we randomly assigned 90% of samples to the train set, and 10% to the test set, and throughout training a 5% validation set will be split from the training set to evaluate the training performance after each epoch. For the TID2013 dataset we split the train/test sets by reference images, to ensure that no reference image, and any of its distortions, enters both the train and test set.

## Serving NIMA with TensorFlow Serving
TensorFlow versions of both the technical and aesthetic MobileNet models are provided,
along with the script to generate them from the original Keras files, under the `contrib/tf_serving` directory.

There is also an already configured TFS `Dockerfile` that you can use.

To get predictions from the aesthetic or technical model:
1. Build the NIMA TFS Docker image `docker build -t tfs_nima contrib/tf_serving`
2. Run a NIMA TFS container with `docker run -d --name tfs_nima -p 8500:8500 tfs_nima`
3. Install python dependencies to run TF serving sample client
```
virtualenv -p python3 contrib/tf_serving/venv_tfs_nima
source contrib/tf_serving/venv_tfs_nima/bin/activate
pip install -r contrib/tf_serving/requirements.txt
```
4. Get predictions from aesthetic or technical model by running the sample client
```
python -m contrib.tf_serving.tfs_sample_client --image-path src/tests/test_images/42039.jpg --model-name mobilenet_aesthetic
python -m contrib.tf_serving.tfs_sample_client --image-path src/tests/test_images/42039.jpg --model-name mobilenet_technical
```


## Maintainers
* Christopher Lennan, github: [clennan](https://github.com/clennan)
* Hao Nguyen, github: [MrBanhBao](https://github.com/MrBanhBao)
Expand Down
13 changes: 13 additions & 0 deletions contrib/tf_serving/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM tensorflow/serving:latest

WORKDIR /tf_serving

# copy project files
COPY tfs_models/mobilenet_aesthetic /models/mobilenet_aesthetic/1
COPY tfs_models/mobilenet_technical /models/mobilenet_technical/1
COPY tf_serving_models.cfg /tf_serving/tf_serving_models.cfg

EXPOSE 8500
ENTRYPOINT []

CMD ["tensorflow_model_server" ,"--port=8500", "--model_config_file=tf_serving_models.cfg"]
3 changes: 3 additions & 0 deletions contrib/tf_serving/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Keras==2.2.*
Pillow==5.3.*
tensorflow-serving-api==1.12.*
48 changes: 48 additions & 0 deletions contrib/tf_serving/save_tfs_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import keras.backend as K
import argparse
from keras.applications.mobilenet import DepthwiseConv2D, relu6
from keras.utils.generic_utils import CustomObjectScope
from tensorflow.python.saved_model import builder as saved_model_builder
from tensorflow.python.saved_model import tag_constants
from tensorflow.python.saved_model.signature_def_utils_impl import \
predict_signature_def

from src.handlers.model_builder import Nima


def main(base_model_name, weights_file, export_path):
# Load model and weights
nima = Nima(base_model_name, weights=None)
nima.build()
nima.nima_model.load_weights(weights_file)

# Tell keras that this will be used for making predictions
K.set_learning_phase(0)

# CustomObject required by MobileNet
with CustomObjectScope({'relu6': relu6, 'DepthwiseConv2D': DepthwiseConv2D}):
builder = saved_model_builder.SavedModelBuilder(export_path)
signature = predict_signature_def(
inputs={'input_image': nima.nima_model.input},
outputs={'quality_prediction': nima.nima_model.output}
)

builder.add_meta_graph_and_variables(
sess=K.get_session(),
tags=[tag_constants.SERVING],
signature_def_map={'image_quality': signature}
)
builder.save()

print(f'TF model exported to: {export_path}')


if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-b', '--base-model-name', help='CNN base model name', required=True)
parser.add_argument('-w', '--weights-file', help='path of weights file', required=True)
parser.add_argument('-ep', '--export-path', help='path to save the tfs model', required=True)

args = parser.parse_args()

main(**args.__dict__)
13 changes: 13 additions & 0 deletions contrib/tf_serving/tf_serving_models.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
model_config_list: {

config: {
name: "mobilenet_technical",
base_path: "/models/mobilenet_technical",
model_platform: "tensorflow"
},
config: {
name: "mobilenet_aesthetic",
base_path: "/models/mobilenet_aesthetic",
model_platform: "tensorflow"
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
53 changes: 53 additions & 0 deletions contrib/tf_serving/tfs_sample_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import json
import argparse
import keras
import numpy as np
import tensorflow as tf
from src.utils import utils
from grpc.beta import implementations
from tensorflow_serving.apis import predict_pb2, prediction_service_pb2

TFS_HOST = 'localhost'
TFS_PORT = 8500


def normalize_labels(labels):
labels_np = np.array(labels)
return labels_np / labels_np.sum()


def calc_mean_score(score_dist):
score_dist = normalize_labels(score_dist)
return (score_dist * np.arange(1, 11)).sum()


def get_image_quality_predictions(image_path, model_name):
# Load and preprocess image
image = utils.load_image(image_path, target_size=(224, 224))
image = keras.applications.mobilenet.preprocess_input(image)

# Run through model
channel = implementations.insecure_channel(TFS_HOST, TFS_PORT)
stub = prediction_service_pb2.beta_create_PredictionService_stub(channel)
request = predict_pb2.PredictRequest()
request.model_spec.name = model_name
request.model_spec.signature_name = 'image_quality'

request.inputs['input_image'].CopyFrom(
tf.contrib.util.make_tensor_proto(np.expand_dims(image, 0))
)

response = stub.Predict(request, 10.0)
result = round(calc_mean_score(response.outputs['quality_prediction'].float_val), 2)

print(json.dumps({'mean_score_prediction': np.round(result, 3)}, indent=2))


if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-ip', '--image-path', help='Path to image file.', required=True)
parser.add_argument(
'-mn', '--model-name', help='mobilenet_aesthetic or mobilenet_technical', required=True
)
args = parser.parse_args()
get_image_quality_predictions(**args.__dict__)
78 changes: 78 additions & 0 deletions contrib/tf_serving/venv_tfs_nima/bin/activate
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# This file must be used with "source bin/activate" *from bash*
# you cannot run it directly

deactivate () {
unset -f pydoc >/dev/null 2>&1

# reset old environment variables
# ! [ -z ${VAR+_} ] returns true if VAR is declared at all
if ! [ -z "${_OLD_VIRTUAL_PATH+_}" ] ; then
PATH="$_OLD_VIRTUAL_PATH"
export PATH
unset _OLD_VIRTUAL_PATH
fi
if ! [ -z "${_OLD_VIRTUAL_PYTHONHOME+_}" ] ; then
PYTHONHOME="$_OLD_VIRTUAL_PYTHONHOME"
export PYTHONHOME
unset _OLD_VIRTUAL_PYTHONHOME
fi

# This should detect bash and zsh, which have a hash command that must
# be called to get it to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ] ; then
hash -r 2>/dev/null
fi

if ! [ -z "${_OLD_VIRTUAL_PS1+_}" ] ; then
PS1="$_OLD_VIRTUAL_PS1"
export PS1
unset _OLD_VIRTUAL_PS1
fi

unset VIRTUAL_ENV
if [ ! "${1-}" = "nondestructive" ] ; then
# Self destruct!
unset -f deactivate
fi
}

# unset irrelevant variables
deactivate nondestructive

VIRTUAL_ENV="/Users/christopher.lennan/github_repos/image-quality-assessment/contrib/tf_serving/venv_tfs_nima"
export VIRTUAL_ENV

_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/bin:$PATH"
export PATH

# unset PYTHONHOME if set
if ! [ -z "${PYTHONHOME+_}" ] ; then
_OLD_VIRTUAL_PYTHONHOME="$PYTHONHOME"
unset PYTHONHOME
fi

if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT-}" ] ; then
_OLD_VIRTUAL_PS1="$PS1"
if [ "x" != x ] ; then
PS1="$PS1"
else
PS1="(`basename \"$VIRTUAL_ENV\"`) $PS1"
fi
export PS1
fi

# Make sure to unalias pydoc if it's already there
alias pydoc 2>/dev/null >/dev/null && unalias pydoc

pydoc () {
python -m pydoc "$@"
}

# This should detect bash and zsh, which have a hash command that must
# be called to get it to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ] ; then
hash -r 2>/dev/null
fi
36 changes: 36 additions & 0 deletions contrib/tf_serving/venv_tfs_nima/bin/activate.csh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# This file must be used with "source bin/activate.csh" *from csh*.
# You cannot run it directly.
# Created by Davide Di Blasi <[email protected]>.

alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; test "\!:*" != "nondestructive" && unalias deactivate && unalias pydoc'

# Unset irrelevant variables.
deactivate nondestructive

setenv VIRTUAL_ENV "/Users/christopher.lennan/github_repos/image-quality-assessment/contrib/tf_serving/venv_tfs_nima"

set _OLD_VIRTUAL_PATH="$PATH"
setenv PATH "$VIRTUAL_ENV/bin:$PATH"



if ("" != "") then
set env_name = ""
else
set env_name = `basename "$VIRTUAL_ENV"`
endif

# Could be in a non-interactive environment,
# in which case, $prompt is undefined and we wouldn't
# care about the prompt anyway.
if ( $?prompt ) then
set _OLD_VIRTUAL_PROMPT="$prompt"
set prompt = "[$env_name] $prompt"
endif

unset env_name

alias pydoc python -m pydoc

rehash

76 changes: 76 additions & 0 deletions contrib/tf_serving/venv_tfs_nima/bin/activate.fish
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# This file must be used using `. bin/activate.fish` *within a running fish ( http://fishshell.com ) session*.
# Do not run it directly.

function deactivate -d 'Exit virtualenv mode and return to the normal environment.'
# reset old environment variables
if test -n "$_OLD_VIRTUAL_PATH"
set -gx PATH $_OLD_VIRTUAL_PATH
set -e _OLD_VIRTUAL_PATH
end

if test -n "$_OLD_VIRTUAL_PYTHONHOME"
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
set -e _OLD_VIRTUAL_PYTHONHOME
end

if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
# Set an empty local `$fish_function_path` to allow the removal of `fish_prompt` using `functions -e`.
set -l fish_function_path

# Erase virtualenv's `fish_prompt` and restore the original.
functions -e fish_prompt
functions -c _old_fish_prompt fish_prompt
functions -e _old_fish_prompt
set -e _OLD_FISH_PROMPT_OVERRIDE
end

set -e VIRTUAL_ENV

if test "$argv[1]" != 'nondestructive'
# Self-destruct!
functions -e pydoc
functions -e deactivate
end
end

# Unset irrelevant variables.
deactivate nondestructive

set -gx VIRTUAL_ENV "/Users/christopher.lennan/github_repos/image-quality-assessment/contrib/tf_serving/venv_tfs_nima"

set -gx _OLD_VIRTUAL_PATH $PATH
set -gx PATH "$VIRTUAL_ENV/bin" $PATH

# Unset `$PYTHONHOME` if set.
if set -q PYTHONHOME
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
set -e PYTHONHOME
end

function pydoc
python -m pydoc $argv
end

if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
# Copy the current `fish_prompt` function as `_old_fish_prompt`.
functions -c fish_prompt _old_fish_prompt

function fish_prompt
# Save the current $status, for fish_prompts that display it.
set -l old_status $status

# Prompt override provided?
# If not, just prepend the environment name.
if test -n ""
printf '%s%s' "" (set_color normal)
else
printf '%s(%s) ' (set_color normal) (basename "$VIRTUAL_ENV")
end

# Restore the original $status
echo "exit $old_status" | source
_old_fish_prompt
end

set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
end
34 changes: 34 additions & 0 deletions contrib/tf_serving/venv_tfs_nima/bin/activate_this.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""By using execfile(this_file, dict(__file__=this_file)) you will
activate this virtualenv environment.
This can be used when you must use an existing Python interpreter, not
the virtualenv bin/python
"""

try:
__file__
except NameError:
raise AssertionError(
"You must run this like execfile('path/to/activate_this.py', dict(__file__='path/to/activate_this.py'))")
import sys
import os

old_os_path = os.environ.get('PATH', '')
os.environ['PATH'] = os.path.dirname(os.path.abspath(__file__)) + os.pathsep + old_os_path
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if sys.platform == 'win32':
site_packages = os.path.join(base, 'Lib', 'site-packages')
else:
site_packages = os.path.join(base, 'lib', 'python%s' % sys.version[:3], 'site-packages')
prev_sys_path = list(sys.path)
import site
site.addsitedir(site_packages)
sys.real_prefix = sys.prefix
sys.prefix = base
# Move the added items to the front of the path:
new_sys_path = []
for item in list(sys.path):
if item not in prev_sys_path:
new_sys_path.append(item)
sys.path.remove(item)
sys.path[:0] = new_sys_path
Loading

0 comments on commit a7a01b6

Please sign in to comment.