chore: import upstream snapshot with attribution
cffconvert / validate (push) Has been skipped
License Check / license-check (push) Failing after 2s

This commit is contained in:
wehub-resource-sync
2026-07-13 12:14:16 +08:00
commit 8a852e4b4e
36502 changed files with 9277225 additions and 0 deletions
@@ -0,0 +1,130 @@
# Copyright 2016 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
# Description:
# Keras saving and loading libraries.
load("@xla//third_party/rules_python/python:py_library.bzl", "py_library")
load("//tensorflow:tensorflow.bzl", "py_test")
package(
# copybara:uncomment default_applicable_licenses = ["//tensorflow:license"],
default_visibility = ["//tensorflow:__subpackages__"],
licenses = ["notice"],
)
py_library(
name = "model_utils",
srcs = ["__init__.py"],
strict_deps = True,
deps = [
":export_output",
":export_utils",
":mode_keys",
],
)
py_library(
name = "export_output",
srcs = ["export_output.py"],
strict_deps = True,
deps = [
"//tensorflow/python/framework:constant_op",
"//tensorflow/python/framework:dtypes",
"//tensorflow/python/framework:ops",
"//tensorflow/python/framework:tensor",
"//tensorflow/python/framework:tensor_util",
"//tensorflow/python/saved_model:signature_def_utils",
],
)
py_test(
name = "export_output_test",
srcs = ["export_output_test.py"],
strict_deps = True,
deps = [
":export_output",
"//tensorflow/core:protos_all_py",
"//tensorflow/python/eager:context",
"//tensorflow/python/framework:constant_op",
"//tensorflow/python/framework:dtypes",
"//tensorflow/python/framework:sparse_tensor",
"//tensorflow/python/framework:tensor",
"//tensorflow/python/ops:array_ops",
"//tensorflow/python/ops:control_flow_ops",
"//tensorflow/python/ops:metrics",
"//tensorflow/python/ops:variables",
"//tensorflow/python/platform:client_testlib",
"//tensorflow/python/saved_model:signature_constants",
],
)
py_library(
name = "export_utils",
srcs = ["export_utils.py"],
strict_deps = True,
deps = [
":export_output",
":mode_keys",
"//tensorflow/python/lib/io:file_io",
"//tensorflow/python/ops:op_selector",
"//tensorflow/python/platform:gfile",
"//tensorflow/python/platform:tf_logging",
"//tensorflow/python/saved_model:signature_constants",
"//tensorflow/python/saved_model:signature_def_utils",
"//tensorflow/python/saved_model:tag_constants",
"//tensorflow/python/saved_model:utils",
"//tensorflow/python/util:compat",
"//tensorflow/python/util:nest",
"//tensorflow/python/util:object_identity",
],
)
py_test(
name = "export_test",
srcs = ["export_test.py"],
strict_deps = True,
deps = [
":export_output",
":export_utils",
":mode_keys",
"//tensorflow/python/framework:constant_op",
"//tensorflow/python/framework:dtypes",
"//tensorflow/python/framework:ops",
"//tensorflow/python/framework:test_lib",
"//tensorflow/python/ops:array_ops",
"//tensorflow/python/ops:math_ops",
"//tensorflow/python/platform:client_testlib",
"//tensorflow/python/saved_model:signature_constants",
"//tensorflow/python/saved_model:signature_def_utils",
],
)
py_library(
name = "mode_keys",
srcs = ["mode_keys.py"],
strict_deps = True,
deps = ["//tensorflow/python/util:compat"],
)
py_test(
name = "mode_keys_test",
srcs = ["mode_keys_test.py"],
strict_deps = True,
deps = [
":mode_keys",
"//tensorflow/python/platform:client_testlib",
],
)
@@ -0,0 +1,27 @@
# Copyright 2018 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
# LINT.IfChange
"""Utils for saving a Keras Model to the SavedModel format."""
# pylint: disable=wildcard-import
from tensorflow.python.saved_model.model_utils.export_output import *
from tensorflow.python.saved_model.model_utils.export_utils import build_all_signature_defs
from tensorflow.python.saved_model.model_utils.export_utils import export_outputs_for_mode
from tensorflow.python.saved_model.model_utils.export_utils import EXPORT_TAG_MAP
from tensorflow.python.saved_model.model_utils.export_utils import get_export_outputs
from tensorflow.python.saved_model.model_utils.export_utils import get_temp_export_dir
from tensorflow.python.saved_model.model_utils.export_utils import get_timestamped_export_dir
from tensorflow.python.saved_model.model_utils.export_utils import SIGNATURE_KEY_MAP
# pylint: enable=wildcard-import
# LINT.ThenChange(//tensorflow/python/keras/saving/utils_v1/__init__.py)
@@ -0,0 +1,425 @@
# Copyright 2017 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
# LINT.IfChange
"""Classes for different types of export output."""
import abc
from tensorflow.python.framework import constant_op
from tensorflow.python.framework import dtypes
from tensorflow.python.framework import ops
from tensorflow.python.framework import tensor
from tensorflow.python.framework import tensor_util
from tensorflow.python.saved_model import signature_def_utils
class ExportOutput:
"""Represents an output of a model that can be served.
These typically correspond to model heads.
"""
__metaclass__ = abc.ABCMeta
_SEPARATOR_CHAR = '/'
@abc.abstractmethod
def as_signature_def(self, receiver_tensors):
"""Generate a SignatureDef proto for inclusion in a MetaGraphDef.
The SignatureDef will specify outputs as described in this ExportOutput,
and will use the provided receiver_tensors as inputs.
Args:
receiver_tensors: a `Tensor`, or a dict of string to `Tensor`, specifying
input nodes that will be fed.
"""
pass
def _check_output_key(self, key, error_label):
# For multi-head models, the key can be a tuple.
if isinstance(key, tuple):
key = self._SEPARATOR_CHAR.join(key)
if not isinstance(key, str):
raise ValueError(
'{} output key must be a string; got {}.'.format(error_label, key))
return key
def _wrap_and_check_outputs(
self, outputs, single_output_default_name, error_label=None):
"""Wraps raw tensors as dicts and checks type.
Note that we create a new dict here so that we can overwrite the keys
if necessary.
Args:
outputs: A `Tensor` or a dict of string to `Tensor`.
single_output_default_name: A string key for use in the output dict
if the provided `outputs` is a raw tensor.
error_label: descriptive string for use in error messages. If none,
single_output_default_name will be used.
Returns:
A dict of tensors
Raises:
ValueError: if the outputs dict keys are not strings or tuples of strings
or the values are not Tensors.
"""
if not isinstance(outputs, dict):
outputs = {single_output_default_name: outputs}
output_dict = {}
for key, value in outputs.items():
error_name = error_label or single_output_default_name
key = self._check_output_key(key, error_name)
if not isinstance(value, tensor.Tensor):
raise ValueError(
'{} output value must be a Tensor; got {}.'.format(
error_name, value))
output_dict[key] = value
return output_dict
class ClassificationOutput(ExportOutput):
"""Represents the output of a classification head.
Either classes or scores or both must be set.
The classes `Tensor` must provide string labels, not integer class IDs.
If only classes is set, it is interpreted as providing top-k results in
descending order.
If only scores is set, it is interpreted as providing a score for every class
in order of class ID.
If both classes and scores are set, they are interpreted as zipped, so each
score corresponds to the class at the same index. Clients should not depend
on the order of the entries.
"""
def __init__(self, scores=None, classes=None):
"""Constructor for `ClassificationOutput`.
Args:
scores: A float `Tensor` giving scores (sometimes but not always
interpretable as probabilities) for each class. May be `None`, but
only if `classes` is set. Interpretation varies-- see class doc.
classes: A string `Tensor` giving predicted class labels. May be `None`,
but only if `scores` is set. Interpretation varies-- see class doc.
Raises:
ValueError: if neither classes nor scores is set, or one of them is not a
`Tensor` with the correct dtype.
"""
if (scores is not None
and not (isinstance(scores, tensor.Tensor)
and scores.dtype.is_floating)):
raise ValueError('Classification scores must be a float32 Tensor; '
'got {}'.format(scores))
if (classes is not None
and not (isinstance(classes, tensor.Tensor)
and dtypes.as_dtype(classes.dtype) == dtypes.string)):
raise ValueError('Classification classes must be a string Tensor; '
'got {}'.format(classes))
if scores is None and classes is None:
raise ValueError('Cannot create a ClassificationOutput with empty '
'arguments. At least one of `scores` and `classes` '
'must be defined.')
self._scores = scores
self._classes = classes
@property
def scores(self):
return self._scores
@property
def classes(self):
return self._classes
def as_signature_def(self, receiver_tensors):
if len(receiver_tensors) != 1:
raise ValueError(
'Classification signatures can only accept a single tensor input of '
'type tf.string. Please check to make sure that you have structured '
'the serving_input_receiver_fn so that it creates a single string '
'placeholder. If your model function expects multiple inputs, then '
'use `tf.io.parse_example()` to parse the string into multiple '
f'tensors.\n Received: {receiver_tensors}')
(_, examples), = receiver_tensors.items()
if dtypes.as_dtype(examples.dtype) != dtypes.string:
raise ValueError(
'Classification signatures can only accept a single tensor input of '
'type tf.string. Please check to make sure that you have structured '
'the serving_input_receiver_fn so that it creates a single string '
'placeholder. If your model function expects multiple inputs, then '
'use `tf.io.parse_example()` to parse the string into multiple '
f'tensors.\n Received: {receiver_tensors}')
return signature_def_utils.classification_signature_def(
examples, self.classes, self.scores)
class RegressionOutput(ExportOutput):
"""Represents the output of a regression head."""
def __init__(self, value):
"""Constructor for `RegressionOutput`.
Args:
value: a float `Tensor` giving the predicted values. Required.
Raises:
ValueError: if the value is not a `Tensor` with dtype tf.float32.
"""
if not (isinstance(value, tensor.Tensor) and value.dtype.is_floating):
raise ValueError('Regression output value must be a float32 Tensor; '
'got {}'.format(value))
self._value = value
@property
def value(self):
return self._value
def as_signature_def(self, receiver_tensors):
if len(receiver_tensors) != 1:
raise ValueError(
'Regression signatures can only accept a single tensor input of '
'type tf.string. Please check to make sure that you have structured '
'the serving_input_receiver_fn so that it creates a single string '
'placeholder. If your model function expects multiple inputs, then '
'use `tf.io.parse_example()` to parse the string into multiple '
f'tensors.\n Received: {receiver_tensors}')
(_, examples), = receiver_tensors.items()
if dtypes.as_dtype(examples.dtype) != dtypes.string:
raise ValueError(
'Regression signatures can only accept a single tensor input of '
'type tf.string. Please check to make sure that you have structured '
'the serving_input_receiver_fn so that it creates a single string '
'placeholder. If your model function expects multiple inputs, then '
'use `tf.io.parse_example()` to parse the string into multiple '
f'tensors.\n Received: {receiver_tensors}')
return signature_def_utils.regression_signature_def(examples, self.value)
class PredictOutput(ExportOutput):
"""Represents the output of a generic prediction head.
A generic prediction need not be either a classification or a regression.
Named outputs must be provided as a dict from string to `Tensor`,
"""
_SINGLE_OUTPUT_DEFAULT_NAME = 'output'
def __init__(self, outputs):
"""Constructor for PredictOutput.
Args:
outputs: A `Tensor` or a dict of string to `Tensor` representing the
predictions.
Raises:
ValueError: if the outputs is not dict, or any of its keys are not
strings, or any of its values are not `Tensor`s.
"""
self._outputs = self._wrap_and_check_outputs(
outputs, self._SINGLE_OUTPUT_DEFAULT_NAME, error_label='Prediction')
@property
def outputs(self):
return self._outputs
def as_signature_def(self, receiver_tensors):
return signature_def_utils.predict_signature_def(receiver_tensors,
self.outputs)
class _SupervisedOutput(ExportOutput):
"""Represents the output of a supervised training or eval process."""
__metaclass__ = abc.ABCMeta
LOSS_NAME = 'loss'
PREDICTIONS_NAME = 'predictions'
METRICS_NAME = 'metrics'
METRIC_VALUE_SUFFIX = 'value'
METRIC_UPDATE_SUFFIX = 'update_op'
_loss = None
_predictions = None
_metrics = None
def __init__(self, loss=None, predictions=None, metrics=None):
"""Constructor for SupervisedOutput (ie, Train or Eval output).
Args:
loss: dict of Tensors or single Tensor representing calculated loss.
predictions: dict of Tensors or single Tensor representing model
predictions.
metrics: Dict of metric results keyed by name.
The values of the dict can be one of the following:
(1) instance of `Metric` class.
(2) (metric_value, update_op) tuples, or a single tuple.
metric_value must be a Tensor, and update_op must be a Tensor or Op.
Raises:
ValueError: if any of the outputs' dict keys are not strings or tuples of
strings or the values are not Tensors (or Operations in the case of
update_op).
"""
if loss is not None:
loss_dict = self._wrap_and_check_outputs(loss, self.LOSS_NAME)
self._loss = self._prefix_output_keys(loss_dict, self.LOSS_NAME)
if predictions is not None:
pred_dict = self._wrap_and_check_outputs(
predictions, self.PREDICTIONS_NAME)
self._predictions = self._prefix_output_keys(
pred_dict, self.PREDICTIONS_NAME)
if metrics is not None:
self._metrics = self._wrap_and_check_metrics(metrics)
def _prefix_output_keys(self, output_dict, output_name):
"""Prepend output_name to the output_dict keys if it doesn't exist.
This produces predictable prefixes for the pre-determined outputs
of SupervisedOutput.
Args:
output_dict: dict of string to Tensor, assumed valid.
output_name: prefix string to prepend to existing keys.
Returns:
dict with updated keys and existing values.
"""
new_outputs = {}
for key, val in output_dict.items():
key = self._prefix_key(key, output_name)
new_outputs[key] = val
return new_outputs
def _prefix_key(self, key, output_name):
if key.find(output_name) != 0:
key = output_name + self._SEPARATOR_CHAR + key
return key
def _wrap_and_check_metrics(self, metrics):
"""Handle the saving of metrics.
Metrics is either a tuple of (value, update_op), or a dict of such tuples.
Here, we separate out the tuples and create a dict with names to tensors.
Args:
metrics: Dict of metric results keyed by name.
The values of the dict can be one of the following:
(1) instance of `Metric` class.
(2) (metric_value, update_op) tuples, or a single tuple.
metric_value must be a Tensor, and update_op must be a Tensor or Op.
Returns:
dict of output_names to tensors
Raises:
ValueError: if the dict key is not a string, or the metric values or ops
are not tensors.
"""
if not isinstance(metrics, dict):
metrics = {self.METRICS_NAME: metrics}
outputs = {}
for key, value in metrics.items():
if isinstance(value, tuple):
metric_val, metric_op = value
else: # value is a keras.Metrics object
metric_val = value.result()
assert len(value.updates) == 1 # We expect only one update op.
metric_op = value.updates[0]
key = self._check_output_key(key, self.METRICS_NAME)
key = self._prefix_key(key, self.METRICS_NAME)
val_name = key + self._SEPARATOR_CHAR + self.METRIC_VALUE_SUFFIX
op_name = key + self._SEPARATOR_CHAR + self.METRIC_UPDATE_SUFFIX
if not isinstance(metric_val, tensor.Tensor):
raise ValueError(
'{} output value must be a Tensor; got {}.'.format(
key, metric_val))
if not (tensor_util.is_tf_type(metric_op) or
isinstance(metric_op, ops.Operation)):
raise ValueError(
'{} update_op must be a Tensor or Operation; got {}.'.format(
key, metric_op))
# We must wrap any ops (or variables) in a Tensor before export, as the
# SignatureDef proto expects tensors only. See b/109740581
metric_op_tensor = metric_op
if not isinstance(metric_op, tensor.Tensor):
with ops.control_dependencies([metric_op]):
metric_op_tensor = constant_op.constant([], name='metric_op_wrapper')
outputs[val_name] = metric_val
outputs[op_name] = metric_op_tensor
return outputs
@property
def loss(self):
return self._loss
@property
def predictions(self):
return self._predictions
@property
def metrics(self):
return self._metrics
@abc.abstractmethod
def _get_signature_def_fn(self):
"""Returns a function that produces a SignatureDef given desired outputs."""
pass
def as_signature_def(self, receiver_tensors):
signature_def_fn = self._get_signature_def_fn()
return signature_def_fn(
receiver_tensors, self.loss, self.predictions, self.metrics)
class TrainOutput(_SupervisedOutput):
"""Represents the output of a supervised training process.
This class generates the appropriate signature def for exporting
training output by type-checking and wrapping loss, predictions, and metrics
values.
"""
def _get_signature_def_fn(self):
return signature_def_utils.supervised_train_signature_def
class EvalOutput(_SupervisedOutput):
"""Represents the output of a supervised eval process.
This class generates the appropriate signature def for exporting
eval output by type-checking and wrapping loss, predictions, and metrics
values.
"""
def _get_signature_def_fn(self):
return signature_def_utils.supervised_eval_signature_def
@@ -0,0 +1,400 @@
# Copyright 2017 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Tests for export."""
from tensorflow.core.framework import tensor_shape_pb2
from tensorflow.core.framework import types_pb2
from tensorflow.core.protobuf import meta_graph_pb2
from tensorflow.python.eager import context
from tensorflow.python.framework import constant_op
from tensorflow.python.framework import dtypes
from tensorflow.python.framework import sparse_tensor
from tensorflow.python.framework import tensor
from tensorflow.python.ops import array_ops
from tensorflow.python.ops import control_flow_ops
from tensorflow.python.ops import metrics as metrics_module
from tensorflow.python.ops import variables
from tensorflow.python.platform import test
from tensorflow.python.saved_model import signature_constants
from tensorflow.python.saved_model.model_utils import export_output as export_output_lib
class ExportOutputTest(test.TestCase):
def test_regress_value_must_be_float(self):
with context.graph_mode():
value = array_ops.placeholder(dtypes.string, 1, name='output-tensor-1')
with self.assertRaisesRegex(
ValueError, 'Regression output value must be a float32 Tensor'):
export_output_lib.RegressionOutput(value)
def test_classify_classes_must_be_strings(self):
with context.graph_mode():
classes = array_ops.placeholder(dtypes.float32, 1, name='output-tensor-1')
with self.assertRaisesRegex(
ValueError, 'Classification classes must be a string Tensor'):
export_output_lib.ClassificationOutput(classes=classes)
def test_classify_scores_must_be_float(self):
with context.graph_mode():
scores = array_ops.placeholder(dtypes.string, 1, name='output-tensor-1')
with self.assertRaisesRegex(
ValueError, 'Classification scores must be a float32 Tensor'):
export_output_lib.ClassificationOutput(scores=scores)
def test_classify_requires_classes_or_scores(self):
with self.assertRaisesRegex(
ValueError,
'Cannot create a ClassificationOutput with empty arguments'):
export_output_lib.ClassificationOutput()
def test_build_standardized_signature_def_regression(self):
with context.graph_mode():
input_tensors = {
'input-1':
array_ops.placeholder(
dtypes.string, 1, name='input-tensor-1')
}
value = array_ops.placeholder(dtypes.float32, 1, name='output-tensor-1')
export_output = export_output_lib.RegressionOutput(value)
actual_signature_def = export_output.as_signature_def(input_tensors)
expected_signature_def = meta_graph_pb2.SignatureDef()
shape = tensor_shape_pb2.TensorShapeProto(
dim=[tensor_shape_pb2.TensorShapeProto.Dim(size=1)])
dtype_float = types_pb2.DataType.Value('DT_FLOAT')
dtype_string = types_pb2.DataType.Value('DT_STRING')
expected_signature_def.inputs[
signature_constants.REGRESS_INPUTS].CopyFrom(
meta_graph_pb2.TensorInfo(name='input-tensor-1:0',
dtype=dtype_string,
tensor_shape=shape))
expected_signature_def.outputs[
signature_constants.REGRESS_OUTPUTS].CopyFrom(
meta_graph_pb2.TensorInfo(name='output-tensor-1:0',
dtype=dtype_float,
tensor_shape=shape))
expected_signature_def.method_name = (
signature_constants.REGRESS_METHOD_NAME)
self.assertEqual(actual_signature_def, expected_signature_def)
def test_build_standardized_signature_def_classify_classes_only(self):
"""Tests classification with one output tensor."""
with context.graph_mode():
input_tensors = {
'input-1':
array_ops.placeholder(
dtypes.string, 1, name='input-tensor-1')
}
classes = array_ops.placeholder(dtypes.string, 1, name='output-tensor-1')
export_output = export_output_lib.ClassificationOutput(classes=classes)
actual_signature_def = export_output.as_signature_def(input_tensors)
expected_signature_def = meta_graph_pb2.SignatureDef()
shape = tensor_shape_pb2.TensorShapeProto(
dim=[tensor_shape_pb2.TensorShapeProto.Dim(size=1)])
dtype_string = types_pb2.DataType.Value('DT_STRING')
expected_signature_def.inputs[
signature_constants.CLASSIFY_INPUTS].CopyFrom(
meta_graph_pb2.TensorInfo(name='input-tensor-1:0',
dtype=dtype_string,
tensor_shape=shape))
expected_signature_def.outputs[
signature_constants.CLASSIFY_OUTPUT_CLASSES].CopyFrom(
meta_graph_pb2.TensorInfo(name='output-tensor-1:0',
dtype=dtype_string,
tensor_shape=shape))
expected_signature_def.method_name = (
signature_constants.CLASSIFY_METHOD_NAME)
self.assertEqual(actual_signature_def, expected_signature_def)
def test_build_standardized_signature_def_classify_both(self):
"""Tests multiple output tensors that include classes and scores."""
with context.graph_mode():
input_tensors = {
'input-1':
array_ops.placeholder(
dtypes.string, 1, name='input-tensor-1')
}
classes = array_ops.placeholder(dtypes.string, 1,
name='output-tensor-classes')
scores = array_ops.placeholder(dtypes.float32, 1,
name='output-tensor-scores')
export_output = export_output_lib.ClassificationOutput(
scores=scores, classes=classes)
actual_signature_def = export_output.as_signature_def(input_tensors)
expected_signature_def = meta_graph_pb2.SignatureDef()
shape = tensor_shape_pb2.TensorShapeProto(
dim=[tensor_shape_pb2.TensorShapeProto.Dim(size=1)])
dtype_float = types_pb2.DataType.Value('DT_FLOAT')
dtype_string = types_pb2.DataType.Value('DT_STRING')
expected_signature_def.inputs[
signature_constants.CLASSIFY_INPUTS].CopyFrom(
meta_graph_pb2.TensorInfo(name='input-tensor-1:0',
dtype=dtype_string,
tensor_shape=shape))
expected_signature_def.outputs[
signature_constants.CLASSIFY_OUTPUT_CLASSES].CopyFrom(
meta_graph_pb2.TensorInfo(name='output-tensor-classes:0',
dtype=dtype_string,
tensor_shape=shape))
expected_signature_def.outputs[
signature_constants.CLASSIFY_OUTPUT_SCORES].CopyFrom(
meta_graph_pb2.TensorInfo(name='output-tensor-scores:0',
dtype=dtype_float,
tensor_shape=shape))
expected_signature_def.method_name = (
signature_constants.CLASSIFY_METHOD_NAME)
self.assertEqual(actual_signature_def, expected_signature_def)
def test_build_standardized_signature_def_classify_scores_only(self):
"""Tests classification without classes tensor."""
with context.graph_mode():
input_tensors = {
'input-1':
array_ops.placeholder(
dtypes.string, 1, name='input-tensor-1')
}
scores = array_ops.placeholder(dtypes.float32, 1,
name='output-tensor-scores')
export_output = export_output_lib.ClassificationOutput(
scores=scores)
actual_signature_def = export_output.as_signature_def(input_tensors)
expected_signature_def = meta_graph_pb2.SignatureDef()
shape = tensor_shape_pb2.TensorShapeProto(
dim=[tensor_shape_pb2.TensorShapeProto.Dim(size=1)])
dtype_float = types_pb2.DataType.Value('DT_FLOAT')
dtype_string = types_pb2.DataType.Value('DT_STRING')
expected_signature_def.inputs[
signature_constants.CLASSIFY_INPUTS].CopyFrom(
meta_graph_pb2.TensorInfo(name='input-tensor-1:0',
dtype=dtype_string,
tensor_shape=shape))
expected_signature_def.outputs[
signature_constants.CLASSIFY_OUTPUT_SCORES].CopyFrom(
meta_graph_pb2.TensorInfo(name='output-tensor-scores:0',
dtype=dtype_float,
tensor_shape=shape))
expected_signature_def.method_name = (
signature_constants.CLASSIFY_METHOD_NAME)
self.assertEqual(actual_signature_def, expected_signature_def)
def test_predict_outputs_valid(self):
"""Tests that no errors are raised when provided outputs are valid."""
outputs = {
'output0': constant_op.constant([0]),
u'output1': constant_op.constant(['foo']),
}
export_output_lib.PredictOutput(outputs)
# Single Tensor is OK too
export_output_lib.PredictOutput(constant_op.constant([0]))
def test_predict_outputs_invalid(self):
with self.assertRaisesRegex(ValueError,
'Prediction output key must be a string'):
export_output_lib.PredictOutput({1: constant_op.constant([0])})
with self.assertRaisesRegex(ValueError,
'Prediction output value must be a Tensor'):
export_output_lib.PredictOutput({
'prediction1': sparse_tensor.SparseTensor(
indices=[[0, 0]], values=[1], dense_shape=[1, 1]),
})
class MockSupervisedOutput(export_output_lib._SupervisedOutput):
"""So that we can test the abstract class methods directly."""
def _get_signature_def_fn(self):
pass
class SupervisedOutputTest(test.TestCase):
def test_supervised_outputs_valid(self):
"""Tests that no errors are raised when provided outputs are valid."""
with context.graph_mode():
loss = {'my_loss': constant_op.constant([0])}
predictions = {u'output1': constant_op.constant(['foo'])}
mean, update_op = metrics_module.mean_tensor(constant_op.constant([0]))
metrics = {
'metrics': (mean, update_op),
'metrics2': (constant_op.constant([0]), constant_op.constant([10]))
}
outputter = MockSupervisedOutput(loss, predictions, metrics)
self.assertEqual(outputter.loss['loss/my_loss'], loss['my_loss'])
self.assertEqual(
outputter.predictions['predictions/output1'], predictions['output1'])
self.assertEqual(outputter.metrics['metrics/update_op'].name,
'mean/update_op:0')
self.assertEqual(
outputter.metrics['metrics2/update_op'], metrics['metrics2'][1])
# Single Tensor is OK too
outputter = MockSupervisedOutput(
loss['my_loss'], predictions['output1'], metrics['metrics'])
self.assertEqual(outputter.loss, {'loss': loss['my_loss']})
self.assertEqual(
outputter.predictions, {'predictions': predictions['output1']})
self.assertEqual(outputter.metrics['metrics/update_op'].name,
'mean/update_op:0')
def test_supervised_outputs_none(self):
outputter = MockSupervisedOutput(
constant_op.constant([0]), None, None)
self.assertLen(outputter.loss, 1)
self.assertIsNone(outputter.predictions)
self.assertIsNone(outputter.metrics)
def test_supervised_outputs_invalid(self):
with self.assertRaisesRegex(ValueError, 'predictions output value must'):
MockSupervisedOutput(constant_op.constant([0]), [3], None)
with self.assertRaisesRegex(ValueError, 'loss output value must'):
MockSupervisedOutput('str', None, None)
with self.assertRaisesRegex(ValueError, 'metrics output value must'):
MockSupervisedOutput(None, None, (15.3, 4))
with self.assertRaisesRegex(ValueError, 'loss output key must'):
MockSupervisedOutput({25: 'Tensor'}, None, None)
def test_supervised_outputs_tuples(self):
"""Tests that no errors are raised when provided outputs are valid."""
with context.graph_mode():
loss = {('my', 'loss'): constant_op.constant([0])}
predictions = {(u'output1', '2'): constant_op.constant(['foo'])}
mean, update_op = metrics_module.mean_tensor(constant_op.constant([0]))
metrics = {
('metrics', '1'): (mean, update_op),
('metrics', '2'): (constant_op.constant([0]),
constant_op.constant([10]))
}
outputter = MockSupervisedOutput(loss, predictions, metrics)
self.assertEqual(set(outputter.loss.keys()), set(['loss/my/loss']))
self.assertEqual(set(outputter.predictions.keys()),
set(['predictions/output1/2']))
self.assertEqual(
set(outputter.metrics.keys()),
set([
'metrics/1/value', 'metrics/1/update_op', 'metrics/2/value',
'metrics/2/update_op'
]))
def test_supervised_outputs_no_prepend(self):
"""Tests that no errors are raised when provided outputs are valid."""
with context.graph_mode():
loss = {'loss': constant_op.constant([0])}
predictions = {u'predictions': constant_op.constant(['foo'])}
mean, update_op = metrics_module.mean_tensor(constant_op.constant([0]))
metrics = {
'metrics_1': (mean, update_op),
'metrics_2': (constant_op.constant([0]), constant_op.constant([10]))
}
outputter = MockSupervisedOutput(loss, predictions, metrics)
self.assertEqual(set(outputter.loss.keys()), set(['loss']))
self.assertEqual(set(outputter.predictions.keys()), set(['predictions']))
self.assertEqual(
set(outputter.metrics.keys()),
set([
'metrics_1/value', 'metrics_1/update_op', 'metrics_2/update_op',
'metrics_2/value'
]))
def test_train_signature_def(self):
with context.graph_mode():
loss = {'my_loss': constant_op.constant([0])}
predictions = {u'output1': constant_op.constant(['foo'])}
mean, update_op = metrics_module.mean_tensor(constant_op.constant([0]))
metrics = {
'metrics_1': (mean, update_op),
'metrics_2': (constant_op.constant([0]), constant_op.constant([10]))
}
outputter = export_output_lib.TrainOutput(loss, predictions, metrics)
receiver = {u'features': constant_op.constant(100, shape=(100, 2)),
'labels': constant_op.constant(100, shape=(100, 1))}
sig_def = outputter.as_signature_def(receiver)
self.assertIn('loss/my_loss', sig_def.outputs)
self.assertIn('metrics_1/value', sig_def.outputs)
self.assertIn('metrics_2/value', sig_def.outputs)
self.assertIn('predictions/output1', sig_def.outputs)
self.assertIn('features', sig_def.inputs)
def test_eval_signature_def(self):
with context.graph_mode():
loss = {'my_loss': constant_op.constant([0])}
predictions = {u'output1': constant_op.constant(['foo'])}
outputter = export_output_lib.EvalOutput(loss, predictions, None)
receiver = {u'features': constant_op.constant(100, shape=(100, 2)),
'labels': constant_op.constant(100, shape=(100, 1))}
sig_def = outputter.as_signature_def(receiver)
self.assertIn('loss/my_loss', sig_def.outputs)
self.assertNotIn('metrics/value', sig_def.outputs)
self.assertIn('predictions/output1', sig_def.outputs)
self.assertIn('features', sig_def.inputs)
def test_metric_op_is_tensor(self):
"""Tests that ops.Operation is wrapped by a tensor for metric_ops."""
with context.graph_mode():
loss = {'my_loss': constant_op.constant([0])}
predictions = {u'output1': constant_op.constant(['foo'])}
mean, update_op = metrics_module.mean_tensor(constant_op.constant([0]))
metrics = {
'metrics_1': (mean, update_op),
'metrics_2': (constant_op.constant([0]), control_flow_ops.no_op()),
# Keras metric's update_state() could return a Variable, rather than
# an Operation or Tensor.
'keras_1': (constant_op.constant([0.5]),
variables.Variable(1.0, name='AssignAddVariableOp_3'))
}
outputter = MockSupervisedOutput(loss, predictions, metrics)
# If we get there, it means constructor succeeded; which is sufficient
# for testing the constructor.
self.assertTrue(outputter.metrics['metrics_1/update_op'].name.startswith(
'mean/update_op'))
self.assertIsInstance(
outputter.metrics['metrics_1/update_op'], tensor.Tensor)
self.assertIsInstance(outputter.metrics['metrics_1/value'], tensor.Tensor)
self.assertEqual(outputter.metrics['metrics_2/value'],
metrics['metrics_2'][0])
self.assertTrue(outputter.metrics['metrics_2/update_op'].name.startswith(
'metric_op_wrapper'))
self.assertIsInstance(
outputter.metrics['metrics_2/update_op'], tensor.Tensor)
if __name__ == '__main__':
test.main()
@@ -0,0 +1,335 @@
# Copyright 2017 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Tests for export utils."""
import os
import tempfile
import time
from tensorflow.python.framework import constant_op
from tensorflow.python.framework import dtypes
from tensorflow.python.framework import ops
from tensorflow.python.framework import test_util
from tensorflow.python.ops import array_ops
from tensorflow.python.ops import math_ops
from tensorflow.python.platform import test
from tensorflow.python.saved_model import signature_constants
from tensorflow.python.saved_model import signature_def_utils
from tensorflow.python.saved_model.model_utils import export_output
from tensorflow.python.saved_model.model_utils import export_utils
from tensorflow.python.saved_model.model_utils.mode_keys import KerasModeKeys
class ExportTest(test_util.TensorFlowTestCase):
def test_build_all_signature_defs_without_receiver_alternatives(self):
# Force the test to run in graph mode.
# This tests a deprecated v1 API that depends on graph-only functions such
# as build_tensor_info.
with ops.Graph().as_default():
receiver_tensor = array_ops.placeholder(dtypes.string)
output_1 = constant_op.constant([1.])
output_2 = constant_op.constant(["2"])
output_3 = constant_op.constant(["3"])
export_outputs = {
signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY:
export_output.RegressionOutput(value=output_1),
"head-2":
export_output.ClassificationOutput(classes=output_2),
"head-3":
export_output.PredictOutput(outputs={"some_output_3": output_3}),
}
signature_defs = export_utils.build_all_signature_defs(
receiver_tensor, export_outputs)
expected_signature_defs = {
"serving_default":
signature_def_utils.regression_signature_def(
receiver_tensor, output_1),
"head-2":
signature_def_utils.classification_signature_def(
receiver_tensor, output_2, None),
"head-3":
signature_def_utils.predict_signature_def(
{"input": receiver_tensor}, {"some_output_3": output_3})
}
self.assertDictEqual(expected_signature_defs, signature_defs)
def test_build_all_signature_defs_with_dict_alternatives(self):
# Force the test to run in graph mode.
# This tests a deprecated v1 API that depends on graph-only functions such
# as build_tensor_info.
with ops.Graph().as_default():
receiver_tensor = array_ops.placeholder(dtypes.string)
receiver_tensors_alternative_1 = {
"foo": array_ops.placeholder(dtypes.int64),
"bar": array_ops.sparse_placeholder(dtypes.float32)
}
unfed_input = array_ops.placeholder(dtypes.bool)
receiver_tensors_alternative_2 = {"unfed": unfed_input}
receiver_tensors_alternatives = {
"other": receiver_tensors_alternative_1,
"with_unfed_input": receiver_tensors_alternative_2
}
output_1 = constant_op.constant([1.])
output_2 = constant_op.constant(["2"])
output_3 = constant_op.constant(["3"])
output_4 = unfed_input
output_5 = math_ops.logical_not(unfed_input)
export_outputs = {
signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY:
export_output.RegressionOutput(value=output_1),
"head-2":
export_output.ClassificationOutput(classes=output_2),
"head-3":
export_output.PredictOutput(outputs={"some_output_3": output_3}),
"head-4":
export_output.PredictOutput(outputs={"some_output_4": output_4}),
"head-5":
export_output.PredictOutput(outputs={"some_output_5": output_5}),
}
signature_defs = export_utils.build_all_signature_defs(
receiver_tensor, export_outputs, receiver_tensors_alternatives)
expected_signature_defs = {
"serving_default":
signature_def_utils.regression_signature_def(
receiver_tensor, output_1),
"head-2":
signature_def_utils.classification_signature_def(
receiver_tensor, output_2, None),
"head-3":
signature_def_utils.predict_signature_def(
{"input": receiver_tensor}, {"some_output_3": output_3}),
"other:head-3":
signature_def_utils.predict_signature_def(
receiver_tensors_alternative_1, {"some_output_3": output_3}),
# Note that the alternatives 'other:serving_default' and
# 'other:head-2' are invalid, because regression and classification
# signatures must take a single string input. Here we verify that
# these invalid signatures are not included in the export_utils.
# Similarly, we verify that 'head-4' and 'head-5', which depend on an
# input that is not being fed as a receiver tensor, are also omitted.
# All the three heads are present when that input is fed, however:
"with_unfed_input:head-3":
signature_def_utils.predict_signature_def(
receiver_tensors_alternative_2, {"some_output_3": output_3}),
"with_unfed_input:head-4":
signature_def_utils.predict_signature_def(
receiver_tensors_alternative_2, {"some_output_4": output_4}),
"with_unfed_input:head-5":
signature_def_utils.predict_signature_def(
receiver_tensors_alternative_2, {"some_output_5": output_5})
}
self.assertDictEqual(expected_signature_defs, signature_defs)
def test_build_all_signature_defs_with_single_alternatives(self):
# Force the test to run in graph mode.
# This tests a deprecated v1 API that depends on graph-only functions such
# as build_tensor_info.
with ops.Graph().as_default():
receiver_tensor = array_ops.placeholder(dtypes.string)
receiver_tensors_alternative_1 = array_ops.placeholder(dtypes.int64)
receiver_tensors_alternative_2 = array_ops.sparse_placeholder(
dtypes.float32)
# Note we are passing single Tensors as values of
# receiver_tensors_alternatives, where normally that is a dict.
# In this case a dict will be created using the default receiver tensor
# name "input".
receiver_tensors_alternatives = {
"other1": receiver_tensors_alternative_1,
"other2": receiver_tensors_alternative_2
}
output_1 = constant_op.constant([1.])
output_2 = constant_op.constant(["2"])
output_3 = constant_op.constant(["3"])
export_outputs = {
signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY:
export_output.RegressionOutput(value=output_1),
"head-2":
export_output.ClassificationOutput(classes=output_2),
"head-3":
export_output.PredictOutput(outputs={"some_output_3": output_3}),
}
signature_defs = export_utils.build_all_signature_defs(
receiver_tensor, export_outputs, receiver_tensors_alternatives)
expected_signature_defs = {
"serving_default":
signature_def_utils.regression_signature_def(
receiver_tensor, output_1),
"head-2":
signature_def_utils.classification_signature_def(
receiver_tensor, output_2, None),
"head-3":
signature_def_utils.predict_signature_def(
{"input": receiver_tensor}, {"some_output_3": output_3}),
"other1:head-3":
signature_def_utils.predict_signature_def(
{"input": receiver_tensors_alternative_1},
{"some_output_3": output_3}),
"other2:head-3":
signature_def_utils.predict_signature_def(
{"input": receiver_tensors_alternative_2},
{"some_output_3": output_3})
# Note that the alternatives 'other:serving_default' and
# 'other:head-2' are invalid, because regression and classification
# signatures must take a single string input. Here we verify that
# these invalid signatures are not included in the export_utils.
}
self.assertDictEqual(expected_signature_defs, signature_defs)
def test_build_all_signature_defs_export_outputs_required(self):
receiver_tensor = constant_op.constant(["11"])
with self.assertRaises(ValueError) as e:
export_utils.build_all_signature_defs(receiver_tensor, None)
self.assertTrue(
str(e.exception).startswith("`export_outputs` must be a dict"))
def test_get_timestamped_export_dir(self):
export_dir_base = tempfile.mkdtemp() + "export/"
export_dir_1 = export_utils.get_timestamped_export_dir(
export_dir_base)
time.sleep(2)
export_dir_2 = export_utils.get_timestamped_export_dir(
export_dir_base)
time.sleep(2)
export_dir_3 = export_utils.get_timestamped_export_dir(
export_dir_base)
# Export directories should be named using a timestamp that is seconds
# since epoch. Such a timestamp is 10 digits long.
time_1 = os.path.basename(export_dir_1)
self.assertEqual(10, len(time_1))
time_2 = os.path.basename(export_dir_2)
self.assertEqual(10, len(time_2))
time_3 = os.path.basename(export_dir_3)
self.assertEqual(10, len(time_3))
self.assertLess(int(time_1), int(time_2))
self.assertLess(int(time_2), int(time_3))
def test_get_temp_export_dir(self):
export_dir = os.path.join("tmp", "export", "1576013284")
tmp_export_dir = export_utils.get_temp_export_dir(export_dir)
self.assertEqual(tmp_export_dir,
os.path.join(b"tmp", b"export", b"temp-1576013284"))
export_dir = os.path.join(b"tmp", b"export", b"1576013284")
tmp_export_dir = export_utils.get_temp_export_dir(export_dir)
self.assertEqual(tmp_export_dir,
os.path.join(b"tmp", b"export", b"temp-1576013284"))
def test_build_all_signature_defs_serving_only(self):
# Force the test to run in graph mode.
# This tests a deprecated v1 API that depends on graph-only functions such
# as build_tensor_info.
with ops.Graph().as_default():
receiver_tensor = {"input": array_ops.placeholder(dtypes.string)}
output_1 = constant_op.constant([1.])
export_outputs = {
signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY:
export_output.PredictOutput(outputs=output_1),
"train":
export_output.TrainOutput(loss=output_1),
}
signature_defs = export_utils.build_all_signature_defs(
receiver_tensor, export_outputs)
expected_signature_defs = {
"serving_default":
signature_def_utils.predict_signature_def(receiver_tensor,
{"output": output_1})
}
self.assertDictEqual(expected_signature_defs, signature_defs)
signature_defs = export_utils.build_all_signature_defs(
receiver_tensor, export_outputs, serving_only=False)
expected_signature_defs.update({
"train":
signature_def_utils.supervised_train_signature_def(
receiver_tensor, loss={"loss": output_1})
})
self.assertDictEqual(expected_signature_defs, signature_defs)
def test_export_outputs_for_mode(self):
predictions = {"predictions": constant_op.constant([1.])}
loss = {"loss": constant_op.constant([2.])}
metrics = {
"metrics": (constant_op.constant([3.]), constant_op.constant([4.]))}
expected_metrics = {
"metrics/value": metrics["metrics"][0],
"metrics/update_op": metrics["metrics"][1]
}
def _build_export_output(mode):
return export_utils.export_outputs_for_mode(
mode, None, predictions, loss, metrics)
ret = _build_export_output(KerasModeKeys.TRAIN)
self.assertIn(signature_constants.DEFAULT_TRAIN_SIGNATURE_DEF_KEY, ret)
export_out = ret[signature_constants.DEFAULT_TRAIN_SIGNATURE_DEF_KEY]
self.assertIsInstance(export_out, export_output.TrainOutput)
self.assertEqual(export_out.predictions, predictions)
self.assertEqual(export_out.loss, loss)
self.assertEqual(export_out.metrics, expected_metrics)
ret = _build_export_output(KerasModeKeys.TEST)
self.assertIn(signature_constants.DEFAULT_EVAL_SIGNATURE_DEF_KEY, ret)
export_out = ret[signature_constants.DEFAULT_EVAL_SIGNATURE_DEF_KEY]
self.assertIsInstance(export_out, export_output.EvalOutput)
self.assertEqual(export_out.predictions, predictions)
self.assertEqual(export_out.loss, loss)
self.assertEqual(export_out.metrics, expected_metrics)
ret = _build_export_output(KerasModeKeys.PREDICT)
self.assertIn(signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY, ret)
export_out = ret[signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY]
self.assertIsInstance(export_out, export_output.PredictOutput)
self.assertEqual(export_out.outputs, predictions)
classes = constant_op.constant(["class5"])
ret = export_utils.export_outputs_for_mode(
KerasModeKeys.PREDICT,
{"classify": export_output.ClassificationOutput(
classes=classes)})
self.assertIn("classify", ret)
export_out = ret["classify"]
self.assertIsInstance(export_out, export_output.ClassificationOutput)
self.assertEqual(export_out.classes, classes)
if __name__ == "__main__":
test.main()
@@ -0,0 +1,409 @@
# Copyright 2017 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Utilities for creating SavedModels."""
import collections
import os
import time
from tensorflow.python.lib.io import file_io
from tensorflow.python.ops import op_selector
from tensorflow.python.platform import gfile
from tensorflow.python.platform import tf_logging as logging
from tensorflow.python.saved_model import signature_constants
from tensorflow.python.saved_model import signature_def_utils
from tensorflow.python.saved_model import tag_constants
from tensorflow.python.saved_model import utils
from tensorflow.python.saved_model.model_utils import export_output as export_output_lib
from tensorflow.python.saved_model.model_utils import mode_keys
from tensorflow.python.saved_model.model_utils.mode_keys import KerasModeKeys as ModeKeys
from tensorflow.python.util import compat
from tensorflow.python.util import nest
from tensorflow.python.util import object_identity
# Mapping of the modes to appropriate MetaGraph tags in the SavedModel.
EXPORT_TAG_MAP = mode_keys.ModeKeyMap(**{
ModeKeys.PREDICT: [tag_constants.SERVING],
ModeKeys.TRAIN: [tag_constants.TRAINING],
ModeKeys.TEST: [tag_constants.EVAL]})
# For every exported mode, a SignatureDef map should be created using the
# functions `export_outputs_for_mode` and `build_all_signature_defs`. By
# default, this map will contain a single Signature that defines the input
# tensors and output predictions, losses, and/or metrics (depending on the mode)
# The default keys used in the SignatureDef map are defined below.
SIGNATURE_KEY_MAP = mode_keys.ModeKeyMap(**{
ModeKeys.PREDICT: signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY,
ModeKeys.TRAIN: signature_constants.DEFAULT_TRAIN_SIGNATURE_DEF_KEY,
ModeKeys.TEST: signature_constants.DEFAULT_EVAL_SIGNATURE_DEF_KEY})
# Default names used in the SignatureDef input map, which maps strings to
# TensorInfo protos.
SINGLE_FEATURE_DEFAULT_NAME = 'feature'
SINGLE_RECEIVER_DEFAULT_NAME = 'input'
SINGLE_LABEL_DEFAULT_NAME = 'label'
### Below utilities are specific to SavedModel exports.
def _must_be_fed(op):
return op.type == 'Placeholder'
def _ensure_servable(input_tensors, names_to_output_tensor_infos):
"""Check that the signature outputs don't depend on unreachable placeholders.
Args:
input_tensors: An iterable of `Tensor`s specified as the signature's inputs.
names_to_output_tensor_infos: An mapping from output names to respective
`TensorInfo`s corresponding to the signature's output tensors.
Raises:
ValueError: If any of the signature's outputs depend on placeholders not
provided as signature's inputs.
"""
plain_input_tensors = nest.flatten(input_tensors, expand_composites=True)
graph = op_selector.get_unique_graph(plain_input_tensors)
output_tensors = [
utils.get_tensor_from_tensor_info(tensor, graph=graph)
for tensor in names_to_output_tensor_infos.values()
]
plain_output_tensors = nest.flatten(output_tensors, expand_composites=True)
dependency_ops = op_selector.get_backward_walk_ops(
plain_output_tensors, stop_at_ts=plain_input_tensors)
fed_tensors = object_identity.ObjectIdentitySet(plain_input_tensors)
for dependency_op in dependency_ops:
if _must_be_fed(dependency_op) and (not all(
output in fed_tensors for output in dependency_op.outputs)):
input_tensor_names = [tensor.name for tensor in plain_input_tensors]
output_tensor_keys = list(names_to_output_tensor_infos.keys())
output_tensor_names = [tensor.name for tensor in plain_output_tensors]
dependency_path = op_selector.show_path(dependency_op,
plain_output_tensors,
plain_input_tensors)
raise ValueError(
f'The signature\'s input tensors {input_tensor_names} are '
f'insufficient to compute its output keys {output_tensor_keys} '
f'(respectively, tensors {output_tensor_names}) because of the '
f'dependency on `{dependency_op.name}` which is not given as '
'a signature input, as illustrated by the following dependency path: '
f'{dependency_path}')
def build_all_signature_defs(receiver_tensors,
export_outputs,
receiver_tensors_alternatives=None,
serving_only=True):
"""Build `SignatureDef`s for all export outputs.
Args:
receiver_tensors: a `Tensor`, or a dict of string to `Tensor`, specifying
input nodes where this receiver expects to be fed by default. Typically,
this is a single placeholder expecting serialized `tf.Example` protos.
export_outputs: a dict of ExportOutput instances, each of which has
an as_signature_def instance method that will be called to retrieve
the signature_def for all export output tensors.
receiver_tensors_alternatives: a dict of string to additional
groups of receiver tensors, each of which may be a `Tensor` or a dict of
string to `Tensor`. These named receiver tensor alternatives generate
additional serving signatures, which may be used to feed inputs at
different points within the input receiver subgraph. A typical usage is
to allow feeding raw feature `Tensor`s *downstream* of the
tf.io.parse_example() op. Defaults to None.
serving_only: boolean; if true, resulting signature defs will only include
valid serving signatures. If false, all requested signatures will be
returned.
Returns:
signature_def representing all passed args.
Raises:
ValueError: if export_outputs is not a dict
"""
if not isinstance(receiver_tensors, dict):
receiver_tensors = {SINGLE_RECEIVER_DEFAULT_NAME: receiver_tensors}
if export_outputs is None or not isinstance(export_outputs, dict):
raise ValueError('`export_outputs` must be a dict. Received '
f'{export_outputs} with type '
f'{type(export_outputs).__name__}.')
signature_def_map = {}
excluded_signatures = {}
input_tensors = receiver_tensors.values()
for output_key, export_output in export_outputs.items():
signature_name = '{}'.format(output_key or 'None')
try:
signature = export_output.as_signature_def(receiver_tensors)
_ensure_servable(input_tensors, signature.outputs)
signature_def_map[signature_name] = signature
except ValueError as e:
excluded_signatures[signature_name] = str(e)
if receiver_tensors_alternatives:
for receiver_name, receiver_tensors_alt in (
receiver_tensors_alternatives.items()):
if not isinstance(receiver_tensors_alt, dict):
receiver_tensors_alt = {
SINGLE_RECEIVER_DEFAULT_NAME: receiver_tensors_alt
}
alt_input_tensors = receiver_tensors_alt.values()
for output_key, export_output in export_outputs.items():
signature_name = '{}:{}'.format(receiver_name or 'None', output_key or
'None')
try:
signature = export_output.as_signature_def(receiver_tensors_alt)
_ensure_servable(alt_input_tensors, signature.outputs)
signature_def_map[signature_name] = signature
except ValueError as e:
excluded_signatures[signature_name] = str(e)
_log_signature_report(signature_def_map, excluded_signatures)
# The above calls to export_output_lib.as_signature_def should return only
# valid signatures; if there is a validity problem, they raise a ValueError,
# in which case we exclude that signature from signature_def_map above.
# The is_valid_signature check ensures that the signatures produced are
# valid for serving, and acts as an additional sanity check for export
# signatures produced for serving. We skip this check for training and eval
# signatures, which are not intended for serving.
if serving_only:
signature_def_map = {
k: v
for k, v in signature_def_map.items()
if signature_def_utils.is_valid_signature(v)
}
return signature_def_map
_FRIENDLY_METHOD_NAMES = {
signature_constants.CLASSIFY_METHOD_NAME: 'Classify',
signature_constants.REGRESS_METHOD_NAME: 'Regress',
signature_constants.PREDICT_METHOD_NAME: 'Predict',
signature_constants.SUPERVISED_TRAIN_METHOD_NAME: 'Train',
signature_constants.SUPERVISED_EVAL_METHOD_NAME: 'Eval',
}
def _log_signature_report(signature_def_map, excluded_signatures):
"""Log a report of which signatures were produced."""
sig_names_by_method_name = collections.defaultdict(list)
# We'll collect whatever method_names are present, but also we want to make
# sure to output a line for each of the three standard methods even if they
# have no signatures.
for method_name in _FRIENDLY_METHOD_NAMES:
sig_names_by_method_name[method_name] = []
for signature_name, sig in signature_def_map.items():
sig_names_by_method_name[sig.method_name].append(signature_name)
# TODO(b/67733540): consider printing the full signatures, not just names
for method_name, sig_names in sig_names_by_method_name.items():
if method_name in _FRIENDLY_METHOD_NAMES:
method_name = _FRIENDLY_METHOD_NAMES[method_name]
logging.info('Signatures INCLUDED in export for {}: {}'.format(
method_name, sig_names if sig_names else 'None'))
if excluded_signatures:
logging.info('Signatures EXCLUDED from export because they cannot be '
'be served via TensorFlow Serving APIs:')
for signature_name, message in excluded_signatures.items():
logging.info('\'{}\' : {}'.format(signature_name, message))
if not signature_def_map:
logging.warn('Export includes no signatures!')
elif (signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY not in
signature_def_map):
logging.warn('Export includes no default signature!')
# When we create a timestamped directory, there is a small chance that the
# directory already exists because another process is also creating these
# directories. In this case we just wait one second to get a new timestamp and
# try again. If this fails several times in a row, then something is seriously
# wrong.
MAX_DIRECTORY_CREATION_ATTEMPTS = 10
def get_timestamped_export_dir(export_dir_base):
"""Builds a path to a new subdirectory within the base directory.
Each export is written into a new subdirectory named using the
current time. This guarantees monotonically increasing version
numbers even across multiple runs of the pipeline.
The timestamp used is the number of seconds since epoch UTC.
Args:
export_dir_base: A string containing a directory to write the exported
graph and checkpoints.
Returns:
The full path of the new subdirectory (which is not actually created yet).
Raises:
RuntimeError: if repeated attempts fail to obtain a unique timestamped
directory name.
"""
attempts = 0
while attempts < MAX_DIRECTORY_CREATION_ATTEMPTS:
timestamp = int(time.time())
result_dir = file_io.join(
compat.as_bytes(export_dir_base), compat.as_bytes(str(timestamp)))
if not gfile.Exists(result_dir):
# Collisions are still possible (though extremely unlikely): this
# directory is not actually created yet, but it will be almost
# instantly on return from this function.
return result_dir
time.sleep(1)
attempts += 1
logging.warn('Directory {} already exists; retrying (attempt {}/{})'.format(
compat.as_str(result_dir), attempts, MAX_DIRECTORY_CREATION_ATTEMPTS))
raise RuntimeError('Failed to obtain a unique export directory name after '
f'{MAX_DIRECTORY_CREATION_ATTEMPTS} attempts.')
def get_temp_export_dir(timestamped_export_dir):
"""Builds a directory name based on the argument but starting with 'temp-'.
This relies on the fact that TensorFlow Serving ignores subdirectories of
the base directory that can't be parsed as integers.
Args:
timestamped_export_dir: the name of the eventual export directory, e.g.
/foo/bar/<timestamp>
Returns:
A sister directory prefixed with 'temp-', e.g. /foo/bar/temp-<timestamp>.
"""
(dirname, basename) = os.path.split(timestamped_export_dir)
if isinstance(basename, bytes):
str_name = basename.decode('utf-8')
else:
str_name = str(basename)
temp_export_dir = file_io.join(
compat.as_bytes(dirname), compat.as_bytes('temp-{}'.format(str_name)))
return temp_export_dir
def export_outputs_for_mode(
mode, serving_export_outputs=None, predictions=None, loss=None,
metrics=None):
"""Util function for constructing a `ExportOutput` dict given a mode.
The returned dict can be directly passed to `build_all_signature_defs` helper
function as the `export_outputs` argument, used for generating a SignatureDef
map.
Args:
mode: A `ModeKeys` specifying the mode.
serving_export_outputs: Describes the output signatures to be exported to
`SavedModel` and used during serving. Should be a dict or None.
predictions: A dict of Tensors or single Tensor representing model
predictions. This argument is only used if serving_export_outputs is not
set.
loss: A dict of Tensors or single Tensor representing calculated loss.
metrics: A dict of (metric_value, update_op) tuples, or a single tuple.
metric_value must be a Tensor, and update_op must be a Tensor or Op
Returns:
Dictionary mapping the key to an `ExportOutput` object.
The key is the expected SignatureDef key for the mode.
Raises:
ValueError: if an appropriate ExportOutput cannot be found for the mode.
"""
if mode not in SIGNATURE_KEY_MAP:
raise ValueError(
f'Export output type not found for `mode`: {mode}. Expected one of: '
f'{list(SIGNATURE_KEY_MAP.keys())}.')
signature_key = SIGNATURE_KEY_MAP[mode]
if mode_keys.is_predict(mode):
return get_export_outputs(serving_export_outputs, predictions)
elif mode_keys.is_train(mode):
return {signature_key: export_output_lib.TrainOutput(
loss=loss, predictions=predictions, metrics=metrics)}
else:
return {signature_key: export_output_lib.EvalOutput(
loss=loss, predictions=predictions, metrics=metrics)}
def get_export_outputs(export_outputs, predictions):
"""Validate export_outputs or create default export_outputs.
Args:
export_outputs: Describes the output signatures to be exported to
`SavedModel` and used during serving. Should be a dict or None.
predictions: Predictions `Tensor` or dict of `Tensor`.
Returns:
Valid export_outputs dict
Raises:
TypeError: if export_outputs is not a dict or its values are not
ExportOutput instances.
"""
if export_outputs is None:
default_output = export_output_lib.PredictOutput(predictions)
export_outputs = {
signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY: default_output}
if not isinstance(export_outputs, dict):
raise TypeError(
f'`export_outputs` must be dict, received: {export_outputs}.')
for v in export_outputs.values():
if not isinstance(v, export_output_lib.ExportOutput):
raise TypeError(
'Values in `export_outputs` must be ExportOutput objects, '
f'received: {export_outputs}.')
_maybe_add_default_serving_output(export_outputs)
return export_outputs
def _maybe_add_default_serving_output(export_outputs):
"""Add a default serving output to the export_outputs if not present.
Args:
export_outputs: Describes the output signatures to be exported to
`SavedModel` and used during serving. Should be a dict.
Returns:
export_outputs dict with default serving signature added if necessary
Raises:
ValueError: if multiple export_outputs were provided without a default
serving key.
"""
if len(export_outputs) == 1:
(key, value), = export_outputs.items()
if key != signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY:
export_outputs[
signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY] = value
if len(export_outputs) > 1:
if (signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY
not in export_outputs):
raise ValueError(
'Multiple `export_outputs` were provided, but none of them are '
'specified as the default. Use'
'`tf.saved_model.DEFAULT_SERVING_SIGNATURE_DEF_KEY` to '
'specify a default.')
return export_outputs
@@ -0,0 +1,107 @@
# Copyright 2016 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
# LINT.IfChange
"""Utils for managing different mode strings used by Keras and Estimator models.
"""
from tensorflow.python.util.compat import collections_abc
class KerasModeKeys(object):
"""Standard names for model modes.
The following standard keys are defined:
* `TRAIN`: training/fitting mode.
* `TEST`: testing/evaluation mode.
* `PREDICT`: prediction/inference mode.
"""
TRAIN = 'train'
TEST = 'test'
PREDICT = 'predict'
# TODO(kathywu): Remove copy in Estimator after nightlies
class EstimatorModeKeys(object):
"""Standard names for Estimator model modes.
The following standard keys are defined:
* `TRAIN`: training/fitting mode.
* `EVAL`: testing/evaluation mode.
* `PREDICT`: predication/inference mode.
"""
TRAIN = 'train'
EVAL = 'eval'
PREDICT = 'infer'
def is_predict(mode):
return mode in [KerasModeKeys.PREDICT, EstimatorModeKeys.PREDICT]
def is_eval(mode):
return mode in [KerasModeKeys.TEST, EstimatorModeKeys.EVAL]
def is_train(mode):
return mode in [KerasModeKeys.TRAIN, EstimatorModeKeys.TRAIN]
class ModeKeyMap(collections_abc.Mapping):
"""Map using ModeKeys as keys.
This class creates an immutable mapping from modes to values. For example,
SavedModel export of Keras and Estimator models use this to map modes to their
corresponding MetaGraph tags/SignatureDef keys.
Since this class uses modes, rather than strings, as keys, both "predict"
(Keras's PREDICT ModeKey) and "infer" (Estimator's PREDICT ModeKey) map to the
same value.
"""
def __init__(self, **kwargs):
self._internal_dict = {}
self._keys = []
for key in kwargs:
self._keys.append(key)
dict_key = self._get_internal_key(key)
if dict_key in self._internal_dict:
raise ValueError(
'Error creating ModeKeyMap. Multiple keys/values found for {} mode.'
.format(dict_key))
self._internal_dict[dict_key] = kwargs[key]
def _get_internal_key(self, key):
"""Return keys used for the internal dictionary."""
if is_train(key):
return KerasModeKeys.TRAIN
if is_eval(key):
return KerasModeKeys.TEST
if is_predict(key):
return KerasModeKeys.PREDICT
raise ValueError('Invalid mode key: {}.'.format(key))
def __getitem__(self, key):
return self._internal_dict[self._get_internal_key(key)]
def __iter__(self):
return iter(self._keys)
def __len__(self):
return len(self._keys)
# LINT.ThenChange(//tensorflow/python/keras/saving/utils_v1/mode_keys.py)
@@ -0,0 +1,61 @@
# Copyright 2019 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""ModeKey Tests."""
from tensorflow.python.platform import test
from tensorflow.python.saved_model.model_utils import mode_keys
class ModeKeyMapTest(test.TestCase):
def test_map(self):
mode_map = mode_keys.ModeKeyMap(**{
mode_keys.KerasModeKeys.PREDICT: 3,
mode_keys.KerasModeKeys.TEST: 1
})
# Test dictionary __getitem__
self.assertEqual(3, mode_map[mode_keys.KerasModeKeys.PREDICT])
self.assertEqual(3, mode_map[mode_keys.EstimatorModeKeys.PREDICT])
self.assertEqual(1, mode_map[mode_keys.KerasModeKeys.TEST])
self.assertEqual(1, mode_map[mode_keys.EstimatorModeKeys.EVAL])
with self.assertRaises(KeyError):
_ = mode_map[mode_keys.KerasModeKeys.TRAIN]
with self.assertRaises(KeyError):
_ = mode_map[mode_keys.EstimatorModeKeys.TRAIN]
with self.assertRaisesRegex(ValueError, 'Invalid mode'):
_ = mode_map['serve']
# Test common dictionary methods
self.assertLen(mode_map, 2)
self.assertEqual({1, 3}, set(mode_map.values()))
self.assertEqual(
{mode_keys.KerasModeKeys.TEST, mode_keys.KerasModeKeys.PREDICT},
set(mode_map.keys()))
# Map is immutable
with self.assertRaises(TypeError):
mode_map[mode_keys.KerasModeKeys.TEST] = 1 # pylint: disable=unsupported-assignment-operation
def test_invalid_init(self):
with self.assertRaisesRegex(ValueError, 'Multiple keys/values found'):
_ = mode_keys.ModeKeyMap(**{
mode_keys.KerasModeKeys.PREDICT: 3,
mode_keys.EstimatorModeKeys.PREDICT: 1
})
if __name__ == '__main__':
test.main()