diff --git a/docs/apis/packaging-models.md b/docs/apis/packaging-models.md index 7ade2e1fcd..0e310794a4 100644 --- a/docs/apis/packaging-models.md +++ b/docs/apis/packaging-models.md @@ -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 diff --git a/examples/iris/tensorflow/model.py b/examples/iris/tensorflow/model.py new file mode 100644 index 0000000000..5e197be9bf --- /dev/null +++ b/examples/iris/tensorflow/model.py @@ -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) diff --git a/examples/iris/tensorflow/requirements.txt b/examples/iris/tensorflow/requirements.txt new file mode 100644 index 0000000000..8e3c98d33c --- /dev/null +++ b/examples/iris/tensorflow/requirements.txt @@ -0,0 +1,2 @@ +tensorflow +sklearn diff --git a/pkg/workloads/tf_api/api.py b/pkg/workloads/tf_api/api.py index 760779966b..e2e3e4f8fd 100644 --- a/pkg/workloads/tf_api/api.py +++ b/pkg/workloads/tf_api/api.py @@ -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) @@ -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] @@ -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)