Skip to content
Merged
2 changes: 1 addition & 1 deletion docs/apis/packaging-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Zip the exported estimator output in your checkpoint directory:

```text
$ ls export/estimator
$ ls export/estimator/1560263597/
saved_model.pb variables/

$ zip -r model.zip export/estimator
Expand Down
94 changes: 94 additions & 0 deletions examples/iris/tensorflow/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# sources copied/modified from https://github.com/tensorflow/models/blob/master/samples/core/get_started/

import tensorflow as tf
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
import shutil
import os

EXPORT_DIR = "iris_tf_export"


def input_fn(features, labels, batch_size, mode):
"""An input function for training"""
dataset = tf.data.Dataset.from_tensor_slices((features, labels))
if mode == tf.estimator.ModeKeys.TRAIN:
dataset = dataset.shuffle(1000).repeat()
dataset = dataset.batch(batch_size)
dataset_it = dataset.make_one_shot_iterator()
irises, labels = dataset_it.get_next()
return {"irises": irises}, labels


def json_serving_input_fn():
inputs = tf.placeholder(shape=[4], dtype=tf.float64)
features = {"irises": tf.expand_dims(inputs, 0)}
return tf.estimator.export.ServingInputReceiver(features=features, receiver_tensors=inputs)


def my_model(features, labels, mode, params):
"""DNN with three hidden layers and learning_rate=0.1."""
net = features["irises"]
for units in params["hidden_units"]:
net = tf.layers.dense(net, units=units, activation=tf.nn.relu)

logits = tf.layers.dense(net, params["n_classes"], activation=None)

predicted_classes = tf.argmax(logits, 1)
if mode == tf.estimator.ModeKeys.PREDICT:
predictions = {
"class_ids": predicted_classes[:, tf.newaxis],
"probabilities": tf.nn.softmax(logits),
"logits": logits,
}
return tf.estimator.EstimatorSpec(
mode=mode,
predictions=predictions,
export_outputs={
"predict": tf.estimator.export.PredictOutput(
{
"class_ids": predicted_classes[:, tf.newaxis],
"probabilities": tf.nn.softmax(logits),
}
)
},
)

loss = tf.losses.sparse_softmax_cross_entropy(labels=labels, logits=logits)

accuracy = tf.metrics.accuracy(labels=labels, predictions=predicted_classes, name="acc_op")
metrics = {"accuracy": accuracy}
tf.summary.scalar("accuracy", accuracy[1])

if mode == tf.estimator.ModeKeys.EVAL:
return tf.estimator.EstimatorSpec(mode, loss=loss, eval_metric_ops=metrics)

optimizer = tf.train.AdagradOptimizer(learning_rate=0.1)
train_op = optimizer.minimize(loss, global_step=tf.train.get_global_step())
return tf.estimator.EstimatorSpec(mode, loss=loss, train_op=train_op)


iris = load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.8, random_state=42)

classifier = tf.estimator.Estimator(
model_fn=my_model, model_dir=EXPORT_DIR, params={"hidden_units": [10, 10], "n_classes": 3}
)


train_input_fn = lambda: input_fn(X_train, y_train, 100, tf.estimator.ModeKeys.TRAIN)
eval_input_fn = lambda: input_fn(X_test, y_test, 100, tf.estimator.ModeKeys.EVAL)
serving_input_fn = lambda: json_serving_input_fn()
exporter = tf.estimator.FinalExporter("estimator", serving_input_fn, as_text=False)
train_spec = tf.estimator.TrainSpec(train_input_fn, max_steps=1000)
eval_spec = tf.estimator.EvalSpec(eval_input_fn, exporters=[exporter], name="estimator-eval")

tf.estimator.train_and_evaluate(classifier, train_spec, eval_spec)

# zip the estimator export dir (the exported path looks like iris_tf_export/export/estimator/1562353043/)
estimator_dir = EXPORT_DIR + "/export/estimator"
shutil.make_archive("tensorflow", "zip", os.path.join(estimator_dir))

# clean up
shutil.rmtree(EXPORT_DIR)
2 changes: 2 additions & 0 deletions examples/iris/tensorflow/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
tensorflow
sklearn
33 changes: 31 additions & 2 deletions pkg/workloads/tf_api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,30 @@ def predict(deployment_name, api_name):
return jsonify(response)


def validate_model_dir(model_dir):
"""
validates that model_dir has the expected directory tree.

For example (your TF serving version number may be different):

1562353043/
saved_model.pb
variables/
variables.data-00000-of-00001
variables.index
"""
version = os.listdir(model_dir)[0]
if not version.isdigit():
raise UserException(
"No versions of servable default found under base path in model_dir. See docs.cortex.dev for how to properly package your TensorFlow model"
)

if "saved_model.pb" not in os.listdir(os.path.join(model_dir, version)):
raise UserException(
'Expected packaged model to have a "saved_model.pb" file. See docs.cortex.dev for how to properly package your TensorFlow model'
)


def start(args):
ctx = Context(s3_path=args.context, cache_dir=args.cache_dir, workload_id=args.workload_id)

Expand All @@ -406,8 +430,7 @@ def start(args):
package.install_packages(ctx.python_packages, ctx.storage)
if not os.path.isdir(args.model_dir):
ctx.storage.download_and_unzip_external(api["model"], args.model_dir)

if util.is_resource_ref(api["model"]):
else:
package.install_packages(ctx.python_packages, ctx.storage)
model_name = util.get_resource_ref(api["model"])
model = ctx.models[model_name]
Expand Down Expand Up @@ -446,6 +469,12 @@ def start(args):
model["input"]["target_vocab"], None, False
)

try:
validate_model_dir(args.model_dir)
except Exception as e:
logger.exception(e)
sys.exit(1)

channel = grpc.insecure_channel("localhost:" + str(args.tf_serve_port))
local_cache["stub"] = prediction_service_pb2_grpc.PredictionServiceStub(channel)

Expand Down