diff --git a/RELEASE.md b/RELEASE.md index 2c1e3138..e9f813a7 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -14,6 +14,7 @@ limitations under the License. ==============================================================================--> # Current version 0.16.0-dev * Under development + * Removed deprecated LatestModuleExporter and register_module_for_export API # Current version 0.15.0 * Require Python 3.9+. diff --git a/tensorflow_hub/BUILD b/tensorflow_hub/BUILD index 435756dd..68df7469 100644 --- a/tensorflow_hub/BUILD +++ b/tensorflow_hub/BUILD @@ -44,14 +44,13 @@ py_library( srcs_version = "PY3", visibility = ["//visibility:public"], deps = [ + # Dependencies of the tensorflow_hub library. ":image_util", ":keras_layer", ":module", ":module_v2", ":native_module", ":saved_model_module", - "//tensorflow_hub:expect_tensorflow_installed", - ":estimator", ":feature_column", ":feature_column_v2", ":config", @@ -72,31 +71,6 @@ py_library( ], ) -py_library( - name = "estimator", - srcs = ["estimator.py"], - srcs_version = "PY3", - deps = [ - ":tf_utils", - "//tensorflow_hub:expect_tensorflow_compat_v1_estimator_installed", - "//tensorflow_hub:expect_tensorflow_installed", - ], -) - -py_test( - name = "estimator_test", - srcs = ["estimator_test.py"], - python_version = "PY3", - srcs_version = "PY3", - tags = ["notsan"], # b/111624588 - deps = [ - ":tensorflow_hub", - "//tensorflow_hub:expect_tensorflow_compat_v1_estimator_installed", - "//tensorflow_hub:expect_tensorflow_installed", - ":expect_tensorflow_hub_includes_estimator_apis", - ], -) - py_library( name = "feature_column", srcs = ["feature_column.py"], @@ -117,7 +91,6 @@ py_test( ":tensorflow_hub", ":test_utils", "//tensorflow_hub:expect_numpy_installed", - "//tensorflow_hub:expect_tensorflow_compat_v1_estimator_installed", "//tensorflow_hub:expect_tensorflow_installed", ":expect_tensorflow_hub_includes_feature_column_apis", ], @@ -143,7 +116,6 @@ py_test( ":tensorflow_hub", ":test_utils", "//tensorflow_hub:expect_numpy_installed", - "//tensorflow_hub:expect_tensorflow_estimator_installed", "//tensorflow_hub:expect_tensorflow_installed", ":expect_tensorflow_hub_includes_feature_column_v2_apis", ], @@ -439,8 +411,6 @@ py_test( deps = [ ":tensorflow_hub", ":test_utils", - "//tensorflow_hub:expect_tensorflow_compat_v1_estimator_installed", - "//tensorflow_hub:expect_tensorflow_estimator_installed", "//tensorflow_hub:expect_tensorflow_installed", ], ) @@ -461,12 +431,6 @@ py_library( name = "expect_tensorflow_installed", ) -# We expect TensorFlow Hub to support Estimator related APIs (module exporter). -# This is for internal bookkeeping only and will always be true. -py_library( - name = "expect_tensorflow_hub_includes_estimator_apis", -) - # We expect TensorFlow Hub to support feature_column_v2 related APIs. # This is for internal bookkeeping only and will always be true. py_library( @@ -479,18 +443,6 @@ py_library( name = "expect_tensorflow_hub_includes_feature_column_apis", ) -# We expect TensorFlow Estimator to already be installed on the system. -# This is for internal bookkeeping only and will be true by virtue of having TensorFlow installed. -py_library( - name = "expect_tensorflow_estimator_installed", -) - -# We expect TensorFlow V1 Estimator to already be installed on the system. -# This is for internal bookkeeping only and will be true by virtue of having TensorFlow installed. -py_library( - name = "expect_tensorflow_compat_v1_estimator_installed", -) - # We expect numpy to already be installed on the system, e.g. via # `pip install numpy` py_library( @@ -566,6 +518,8 @@ py_library( deps = [":resolver"], ) +# End of BUILD rules. + exports_files([ "copy.bara.sky", ]) diff --git a/tensorflow_hub/__init__.py b/tensorflow_hub/__init__.py index 731d3e17..0a935dd2 100644 --- a/tensorflow_hub/__init__.py +++ b/tensorflow_hub/__init__.py @@ -87,8 +87,7 @@ def _ensure_tf_install(): # pylint: disable=g-import-not-at-top # pylint: disable=g-bad-import-order -from tensorflow_hub.estimator import LatestModuleExporter -from tensorflow_hub.estimator import register_module_for_export +# Symbols exposed via tensorflow_hub. from tensorflow_hub.feature_column_v2 import text_embedding_column_v2 from tensorflow_hub.feature_column import image_embedding_column from tensorflow_hub.feature_column import sparse_text_embedding_column diff --git a/tensorflow_hub/feature_column_test.py b/tensorflow_hub/feature_column_test.py index e84bd288..76206949 100644 --- a/tensorflow_hub/feature_column_test.py +++ b/tensorflow_hub/feature_column_test.py @@ -17,9 +17,7 @@ import os import unittest -import numpy as np import tensorflow as tf -from tensorflow.compat.v1 import estimator as tf_estimator import tensorflow_hub as hub # pylint: disable=g-direct-tensorflow-import @@ -172,36 +170,6 @@ def testDenseFeatures_shareAcrossApplication(self): [5, 5, 5, 5]]) self.assertAllEqual(after_update_1, after_update_2) - def testWorksWithCannedEstimator(self): - comment_embedding_column = hub.text_embedding_column( - "comment", self.spec, trainable=False) - upvotes = tf.compat.v1.feature_column.numeric_column("upvotes") - - feature_columns = [comment_embedding_column, upvotes] - estimator = tf_estimator.DNNClassifier( - hidden_units=[10], - feature_columns=feature_columns, - model_dir=self.get_temp_dir()) - - # This only tests that estimator apis are working with the feature - # column without throwing exceptions. - features = { - "comment": np.array([ - ["the quick brown fox"], - ["spam spam spam"], - ]), - "upvotes": np.array([ - [20], - [1], - ]), - } - labels = np.array([[1], [0]]) - numpy_input_fn = tf_estimator.inputs.numpy_input_fn - input_fn = numpy_input_fn(features, labels, shuffle=True) - estimator.train(input_fn, max_steps=1) - estimator.evaluate(input_fn, steps=1) - estimator.predict(input_fn) - def testTrainableEmbeddingColumn(self): feature_columns = [ hub.text_embedding_column("text", self.spec, trainable=True), @@ -375,33 +343,6 @@ def testDenseFeatures_shareAcrossApplication(self): self.assertAllClose(output_1, output_2) - def testWorksWithCannedEstimator(self): - image_column = hub.image_embedding_column("image", self.spec) - other_column = tf.compat.v1.feature_column.numeric_column("number") - - feature_columns = [image_column, other_column] - estimator = tf_estimator.DNNClassifier( - hidden_units=[10], - feature_columns=feature_columns, - model_dir=self.get_temp_dir()) - - # This only tests that estimator apis are working with the feature - # column without throwing exceptions. - features = { - "image": - np.array([[[[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]], - [[[0.7, 0.7, 0.7], [0.1, 0.2, 0.3]]]], - dtype=np.float32), - "number": - np.array([[20], [1]]), - } - labels = np.array([[1], [0]]) - numpy_input_fn = tf_estimator.inputs.numpy_input_fn - input_fn = numpy_input_fn(features, labels, shuffle=True) - estimator.train(input_fn, max_steps=1) - estimator.evaluate(input_fn, steps=1) - estimator.predict(input_fn) - def testConfig(self): module_path = os.path.join(self.get_temp_dir(), "module") export_module_spec(self.spec, module_path) diff --git a/tensorflow_hub/feature_column_v2_test.py b/tensorflow_hub/feature_column_v2_test.py index 998e989d..fb3b9c0f 100644 --- a/tensorflow_hub/feature_column_v2_test.py +++ b/tensorflow_hub/feature_column_v2_test.py @@ -17,7 +17,6 @@ import logging import os import numpy as np -from tensorflow import estimator as tf_estimator import tensorflow.compat.v2 as tf import tensorflow_hub as hub @@ -172,59 +171,6 @@ def testLoadingDifferentFeatureColumnsFails(self): ".*not bound to checkpointed values.*"): model_2.load_weights(checkpoint_path).assert_consumed() - def testWorksWithTF2DnnClassifier(self): - self.skipTest("b/154115879 - needs more investigation for timeout.") - comment_embedding_column = hub.text_embedding_column_v2( - "comment", self.model, trainable=False) - upvotes = tf.feature_column.numeric_column("upvotes") - - feature_columns = [comment_embedding_column, upvotes] - estimator = tf_estimator.DNNClassifier( - hidden_units=[10], - feature_columns=feature_columns, - model_dir=self.get_temp_dir()) - - # This only tests that estimator apis are working with the feature - # column without throwing exceptions. - def input_fn(): - features = { - "comment": np.array([ - ["the quick brown fox"], - ["spam spam spam"], - ]), - "upvotes": np.array([ - [20], - [1], - ]), - } - labels = np.array([[1], [0]]) - return features, labels - estimator.train(input_fn, max_steps=1) - estimator.evaluate(input_fn, steps=1) - estimator.predict(input_fn) - - def testWorksWithDNNEstimatorAndDataset(self): - self.skipTest("b/154115879 - needs more investigation for timeout.") - description_embeddings = hub.text_embedding_column_v2( - "descriptions", self.model_returning_dicts, output_key="outputs") - - def input_fn(): - features = dict(descriptions=tf.constant([["sentence"]])) - labels = tf.constant([[1]]) - dataset = tf.data.Dataset.from_tensor_slices((features, labels)) - - data_batches = dataset.repeat().take(30).batch(5) - return data_batches - - estimator = tf_estimator.DNNEstimator( - model_dir=os.path.join(self.get_temp_dir(), "estimator_export"), - hidden_units=[10], - head=tf_estimator.BinaryClassHead(), - feature_columns=[description_embeddings]) - - estimator.train(input_fn=input_fn, max_steps=1) - - if __name__ == "__main__": # This test is only supported in TF2 mode and only in TensorFlow version that # has the following symbol (expected from TF2.3 onwards): diff --git a/tensorflow_hub/keras_layer_test.py b/tensorflow_hub/keras_layer_test.py index cd4244a7..b5cfbc1c 100644 --- a/tensorflow_hub/keras_layer_test.py +++ b/tensorflow_hub/keras_layer_test.py @@ -20,8 +20,6 @@ from absl.testing import parameterized import numpy as np import tensorflow as tf -from tensorflow import estimator as tf_estimator -from tensorflow.compat.v1 import estimator as tf_compat_v1_estimator import tensorflow_hub as hub # NOTE: A Hub-style SavedModel can either be constructed manually, or by @@ -712,288 +710,6 @@ def testSaveModelConfig(self, save_from_keras): self.assertEqual(result, new_result) -class EstimatorTest(tf.test.TestCase, parameterized.TestCase): - """Tests use of KerasLayer in an Estimator's model_fn.""" - - def _half_plus_one_model_fn(self, features, labels, mode, params): - inp = features # This estimator takes a single feature, not a dict. - imported = hub.KerasLayer( - params["hub_module"], trainable=params["hub_trainable"]) - model = tf.keras.Sequential([imported]) - outp = model(inp, training=(mode == tf_estimator.ModeKeys.TRAIN)) - # https://www.tensorflow.org/alpha/guide/migration_guide#using_a_custom_model_fn - # recommends model.get_losses_for() instead of model.losses. - model_losses = model.get_losses_for(None) + model.get_losses_for(inp) - regularization_loss = tf.add_n(model_losses or [0.0]) - predictions = dict(output=outp, regularization_loss=regularization_loss) - - total_loss = None - if mode in (tf_estimator.ModeKeys.TRAIN, tf_estimator.ModeKeys.EVAL): - total_loss = tf.add( - tf.compat.v1.losses.mean_squared_error(labels, outp), - regularization_loss) - - train_op = None - if mode == tf_estimator.ModeKeys.TRAIN: - optimizer = tf.compat.v1.train.GradientDescentOptimizer(0.002) - train_op = optimizer.minimize( - total_loss, - var_list=model.trainable_variables, - global_step=tf.compat.v1.train.get_or_create_global_step()) - - return tf_estimator.EstimatorSpec( - mode=mode, predictions=predictions, loss=total_loss, train_op=train_op) - - @parameterized.parameters(("TF2SavedModel_SavedRaw"), - ("TF2SavedModel_SavedFromKeras")) - def testHalfPlusOneRetraining(self, model_format): - export_dir = os.path.join(self.get_temp_dir(), "half-plus-one") - _dispatch_model_format(model_format, _save_half_plus_one_model, - _save_half_plus_one_hub_module_v1, export_dir) - estimator = tf_estimator.Estimator( - model_fn=self._half_plus_one_model_fn, - params=dict(hub_module=export_dir, hub_trainable=True)) - - # The consumer model computes y = x/2 + 1 as expected. - predictions = next( - estimator.predict( - tf_compat_v1_estimator.inputs.numpy_input_fn( - np.array([[0.], [8.], [10.], [12.]], dtype=np.float32), - shuffle=False), - yield_single_examples=False)) - self.assertAllEqual(predictions["output"], - np.array([[1.], [5.], [6.], [7.]], dtype=np.float32)) - self.assertAllEqual(predictions["regularization_loss"], - np.array(0.0025, dtype=np.float32)) - - # Retrain on y = x/2 + 6 for x near 10. - # (Console output should show loss below 0.2.) - x = [[9.], [10.], [11.]] * 10 - y = [[xi[0] / 2. + 6] for xi in x] - estimator.train( - tf_compat_v1_estimator.inputs.numpy_input_fn( - np.array(x, dtype=np.float32), - np.array(y, dtype=np.float32), - batch_size=len(x), - num_epochs=None, - shuffle=False), - steps=10) - # The bias is non-trainable and has to stay at 1.0. - # To compensate, the kernel weight will grow to almost 1.0. - predictions = next( - estimator.predict( - tf_compat_v1_estimator.inputs.numpy_input_fn( - np.array([[0.], [10.]], dtype=np.float32), shuffle=False), - yield_single_examples=False)) - self.assertAllEqual(predictions["output"][0], - np.array([1.], dtype=np.float32)) - self.assertAllClose( - predictions["output"][1], - np.array([11.], dtype=np.float32), - atol=0.0, - rtol=0.03) - self.assertAllClose( - predictions["regularization_loss"], - np.array(0.01, dtype=np.float32), - atol=0.0, - rtol=0.06) - - @parameterized.parameters(("TF2SavedModel_SavedRaw"), - ("TF2SavedModel_SavedFromKeras"), ("TF1HubModule")) - def testHalfPlusOneFrozen(self, model_format): - export_dir = os.path.join(self.get_temp_dir(), "half-plus-one") - _dispatch_model_format(model_format, _save_half_plus_one_model, - _save_half_plus_one_hub_module_v1, export_dir) - estimator = tf_estimator.Estimator( - model_fn=self._half_plus_one_model_fn, - params=dict(hub_module=export_dir, hub_trainable=False)) - - # The consumer model computes y = x/2 + 1 as expected. - predictions = next( - estimator.predict( - tf_compat_v1_estimator.inputs.numpy_input_fn( - np.array([[0.], [8.], [10.], [12.]], dtype=np.float32), - shuffle=False), - yield_single_examples=False)) - self.assertAllEqual(predictions["output"], - np.array([[1.], [5.], [6.], [7.]], dtype=np.float32)) - self.assertAllEqual(predictions["regularization_loss"], - np.array(0.0, dtype=np.float32)) - - def _batch_norm_model_fn(self, features, labels, mode, params): - inp = features # This estimator takes a single feature, not a dict. - imported = hub.KerasLayer(params["hub_module"]) - var_beta, var_gamma, var_mean, var_variance = _get_batch_norm_vars(imported) - if params["train_batch_norm"]: - imported.trainable = True - model = tf.keras.Sequential([imported]) - else: - imported.trainable = False - # When not training the batch norm layer, we train this instead: - dense = tf.keras.layers.Dense( - units=1, - kernel_initializer=tf.keras.initializers.Constant([[1.5]]), - use_bias=False) - model = tf.keras.Sequential([imported, dense]) - outp = model(inp, training=(mode == tf_estimator.ModeKeys.TRAIN)) - predictions = dict( - output=outp, - beta=var_beta.value(), - gamma=var_gamma.value(), - mean=var_mean.value(), - variance=var_variance.value()) - - # https://www.tensorflow.org/alpha/guide/migration_guide#using_a_custom_model_fn - # recommends model.get_updates_for() instead of model.updates. - update_ops = model.get_updates_for(None) + model.get_updates_for(inp) - - loss = None - if mode in (tf_estimator.ModeKeys.TRAIN, tf_estimator.ModeKeys.EVAL): - loss = tf.compat.v1.losses.mean_squared_error(labels, outp) - - train_op = None - if mode == tf_estimator.ModeKeys.TRAIN: - optimizer = tf.compat.v1.train.GradientDescentOptimizer(0.1) - with tf.control_dependencies(update_ops): - train_op = optimizer.minimize( - loss, - var_list=model.trainable_variables, - global_step=tf.compat.v1.train.get_or_create_global_step()) - - return tf_estimator.EstimatorSpec( - mode=mode, predictions=predictions, loss=loss, train_op=train_op) - - @parameterized.named_parameters(("SavedRaw", False), ("SavedFromKeras", True)) - def testBatchNormRetraining(self, save_from_keras): - """Tests imported batch norm with trainable=True.""" - export_dir = os.path.join(self.get_temp_dir(), "batch-norm") - _save_batch_norm_model(export_dir, save_from_keras=save_from_keras) - estimator = tf_estimator.Estimator( - model_fn=self._batch_norm_model_fn, - params=dict(hub_module=export_dir, train_batch_norm=True)) - - # Retrain the imported batch norm layer on a fixed batch of inputs, - # which has mean 12.0 and some variance of a less obvious value. - # The module learns scale and offset parameters that achieve the - # mapping x --> 2*x for the observed mean and variance. - x = [[11.], [12.], [13.]] - y = [[2 * xi[0]] for xi in x] - train_input_fn = tf_compat_v1_estimator.inputs.numpy_input_fn( - np.array(x, dtype=np.float32), - np.array(y, dtype=np.float32), - batch_size=len(x), - num_epochs=None, - shuffle=False) - estimator.train(train_input_fn, steps=100) - predictions = next( - estimator.predict(train_input_fn, yield_single_examples=False)) - self.assertAllClose(predictions["mean"], np.array([12.0])) - self.assertAllClose(predictions["beta"], np.array([24.0])) - self.assertAllClose(predictions["output"], np.array(y)) - - # Evaluating the model operates batch norm in inference mode: - # - Batch statistics are ignored in favor of aggregated statistics, - # computing x --> 2*x independent of input distribution. - # - Update ops are not run, so this doesn't change over time. - predict_input_fn = tf_compat_v1_estimator.inputs.numpy_input_fn( - np.array([[10.], [20.], [30.]], dtype=np.float32), - batch_size=3, - num_epochs=100, - shuffle=False) - for predictions in estimator.predict( - predict_input_fn, yield_single_examples=False): - self.assertAllClose(predictions["output"], np.array([[20.], [40.], - [60.]])) - self.assertAllClose(predictions["mean"], np.array([12.0])) - self.assertAllClose(predictions["beta"], np.array([24.0])) - - @parameterized.named_parameters(("SavedRaw", False), ("SavedFromKeras", True)) - def testBatchNormFreezing(self, save_from_keras): - """Tests imported batch norm with trainable=False.""" - export_dir = os.path.join(self.get_temp_dir(), "batch-norm") - _save_batch_norm_model(export_dir, save_from_keras=save_from_keras) - estimator = tf_estimator.Estimator( - model_fn=self._batch_norm_model_fn, - params=dict(hub_module=export_dir, train_batch_norm=False)) - x = [[1.], [2.], [3.]] - y = [[2 * xi[0]] for xi in x] - input_fn = tf_compat_v1_estimator.inputs.numpy_input_fn( - np.array(x, dtype=np.float32), - np.array(y, dtype=np.float32), - batch_size=len(x), - num_epochs=None, - shuffle=False) - predictions = next(estimator.predict(input_fn, yield_single_examples=False)) - self.assertAllClose(predictions["beta"], np.array([0.0])) - self.assertAllClose(predictions["gamma"], np.array([1.0])) - self.assertAllClose(predictions["mean"], np.array([0.0])) - self.assertAllClose(predictions["variance"], np.array([1.0])) - - # Training the model to x --> 2*x leaves the batch norm layer entirely - # unchanged (both trained beta&gamma and aggregated mean&variance). - estimator.train(input_fn, steps=20) - predictions = next(estimator.predict(input_fn, yield_single_examples=False)) - self.assertAllClose(predictions["beta"], np.array([0.0])) - self.assertAllClose(predictions["gamma"], np.array([1.0])) - self.assertAllClose(predictions["mean"], np.array([0.0])) - self.assertAllClose(predictions["variance"], np.array([1.0])) - self.assertAllClose(predictions["output"], np.array(y)) - - def _output_shape_list_model_fn(self, features, labels, mode, params): - inp = tf.keras.layers.Input(shape=(1,), dtype=tf.float32) - kwargs = {} - if "output_shape" in params: - kwargs["output_shape"] = params["output_shape"] - imported = hub.KerasLayer(params["hub_module"], **kwargs) - outp = imported(inp) - model = tf.keras.Model(inp, outp) - - out_list = model(features, training=(mode == tf_estimator.ModeKeys.TRAIN)) - for j, out in enumerate(out_list): - i = j + 1 # Sample shapes count from one. - actual_shape = out.shape.as_list()[1:] # Without batch size. - expected_shape = [i] * i if "output_shape" in params else [None] * i - self.assertEqual(actual_shape, expected_shape) - predictions = {["one", "two", "three"][i]: out_list[i] for i in range(3)} - imported.get_config() - - return tf_estimator.EstimatorSpec( - mode=mode, predictions=predictions, loss=None, train_op=None) - - @parameterized.named_parameters(("NoOutputShapes", False), - ("WithOutputShapes", True)) - def testOutputShapeList(self, pass_output_shapes): - export_dir = os.path.join(self.get_temp_dir(), "obscurely-shaped") - _save_model_with_obscurely_shaped_list_output(export_dir) - - params = dict(hub_module=export_dir) - if pass_output_shapes: - params["output_shape"] = [[1], [2, 2], [3, 3, 3]] - estimator = tf_estimator.Estimator( - model_fn=self._output_shape_list_model_fn, params=params) - x = [[1.], [10.]] - input_fn = tf_compat_v1_estimator.inputs.numpy_input_fn( - np.array(x, dtype=np.float32), - batch_size=len(x), - num_epochs=None, - shuffle=False) - predictions = next(estimator.predict(input_fn, yield_single_examples=False)) - single = predictions["one"] - double = predictions["two"] - triple = predictions["three"] - self.assertAllClose(single, np.array([[1.], [10.]])) - self.assertAllClose( - double, np.array([[[2., 2.], [2., 2.]], [[20., 20.], [20., 20.]]])) - self.assertAllClose( - triple, - np.array([[[[3., 3., 3.], [3., 3., 3.], [3., 3., 3.]], - [[3., 3., 3.], [3., 3., 3.], [3., 3., 3.]], - [[3., 3., 3.], [3., 3., 3.], [3., 3., 3.]]], - [[[30., 30., 30.], [30., 30., 30.], [30., 30., 30.]], - [[30., 30., 30.], [30., 30., 30.], [30., 30., 30.]], - [[30., 30., 30.], [30., 30., 30.], [30., 30., 30.]]]])) - - class KerasLayerTest(tf.test.TestCase, parameterized.TestCase): """Unit tests for KerasLayer."""