1 # Copyright 2018 The TensorFlow Authors. All Rights Reserved. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 # ============================================================================== 15 # pylint: disable=protected-access 16 """Tests for saving/loading function for keras Model.""" 17 from __future__ import absolute_import 18 from __future__ import division 19 from __future__ import print_function 20 21 import os 22 import shutil 23 24 from absl.testing import parameterized 25 import numpy as np 26 27 from tensorflow.python import keras 28 from tensorflow.python import tf2 29 from tensorflow.python.client import session 30 from tensorflow.python.eager import context 31 from tensorflow.python.framework import dtypes 32 from tensorflow.python.framework import ops 33 from tensorflow.python.framework import tensor_spec 34 from tensorflow.python.framework import test_util 35 from tensorflow.python.keras.engine import training 36 from tensorflow.python.keras.optimizer_v2 import adadelta 37 from tensorflow.python.keras.saving import saved_model as keras_saved_model 38 from tensorflow.python.keras.utils import mode_keys 39 from tensorflow.python.keras.utils import tf_utils 40 from tensorflow.python.ops import array_ops 41 from tensorflow.python.platform import test 42 from tensorflow.python.saved_model import loader_impl 43 from tensorflow.python.saved_model import model_utils 44 from tensorflow.python.training import training as training_module 45 46 47 class TestModelSavingandLoading(test.TestCase): 48 49 def _save_model_dir(self, dirname='saved_model'): 50 temp_dir = self.get_temp_dir() 51 self.addCleanup(shutil.rmtree, temp_dir, ignore_errors=True) 52 return os.path.join(temp_dir, dirname) 53 54 def test_saving_sequential_model(self): 55 with self.cached_session(): 56 model = keras.models.Sequential() 57 model.add(keras.layers.Dense(2, input_shape=(3,))) 58 model.add(keras.layers.RepeatVector(3)) 59 model.add(keras.layers.TimeDistributed(keras.layers.Dense(3))) 60 model.compile( 61 loss=keras.losses.MSE, 62 optimizer=keras.optimizers.RMSprop(lr=0.0001), 63 metrics=[keras.metrics.categorical_accuracy], 64 sample_weight_mode='temporal') 65 x = np.random.random((1, 3)) 66 y = np.random.random((1, 3, 3)) 67 model.train_on_batch(x, y) 68 69 ref_y = model.predict(x) 70 71 saved_model_dir = self._save_model_dir() 72 keras_saved_model.export_saved_model(model, saved_model_dir) 73 74 loaded_model = keras_saved_model.load_from_saved_model(saved_model_dir) 75 y = loaded_model.predict(x) 76 self.assertAllClose(ref_y, y, atol=1e-05) 77 78 @test_util.run_in_graph_and_eager_modes 79 def test_saving_sequential_model_without_compile(self): 80 with self.cached_session(): 81 model = keras.models.Sequential() 82 model.add(keras.layers.Dense(2, input_shape=(3,))) 83 model.add(keras.layers.RepeatVector(3)) 84 model.add(keras.layers.TimeDistributed(keras.layers.Dense(3))) 85 86 x = np.random.random((1, 3)) 87 ref_y = model.predict(x) 88 89 saved_model_dir = self._save_model_dir() 90 keras_saved_model.export_saved_model(model, saved_model_dir) 91 loaded_model = keras_saved_model.load_from_saved_model(saved_model_dir) 92 93 y = loaded_model.predict(x) 94 self.assertAllClose(ref_y, y, atol=1e-05) 95 96 def test_saving_functional_model(self): 97 with self.cached_session(): 98 inputs = keras.layers.Input(shape=(3,)) 99 x = keras.layers.Dense(2)(inputs) 100 output = keras.layers.Dense(3)(x) 101 102 model = keras.models.Model(inputs, output) 103 model.compile( 104 loss=keras.losses.MSE, 105 optimizer=keras.optimizers.RMSprop(lr=0.0001), 106 metrics=[keras.metrics.categorical_accuracy]) 107 x = np.random.random((1, 3)) 108 y = np.random.random((1, 3)) 109 model.train_on_batch(x, y) 110 111 ref_y = model.predict(x) 112 113 saved_model_dir = self._save_model_dir() 114 keras_saved_model.export_saved_model(model, saved_model_dir) 115 loaded_model = keras_saved_model.load_from_saved_model(saved_model_dir) 116 117 y = loaded_model.predict(x) 118 self.assertAllClose(ref_y, y, atol=1e-05) 119 120 @test_util.run_in_graph_and_eager_modes 121 def test_saving_functional_model_without_compile(self): 122 with self.cached_session(): 123 inputs = keras.layers.Input(shape=(3,)) 124 x = keras.layers.Dense(2)(inputs) 125 output = keras.layers.Dense(3)(x) 126 127 model = keras.models.Model(inputs, output) 128 129 x = np.random.random((1, 3)) 130 y = np.random.random((1, 3)) 131 132 ref_y = model.predict(x) 133 134 saved_model_dir = self._save_model_dir() 135 keras_saved_model.export_saved_model(model, saved_model_dir) 136 loaded_model = keras_saved_model.load_from_saved_model(saved_model_dir) 137 138 y = loaded_model.predict(x) 139 self.assertAllClose(ref_y, y, atol=1e-05) 140 141 @test_util.run_in_graph_and_eager_modes 142 def test_saving_with_tf_optimizer(self): 143 model = keras.models.Sequential() 144 model.add(keras.layers.Dense(2, input_shape=(3,))) 145 model.add(keras.layers.Dense(3)) 146 model.compile( 147 loss='mse', 148 optimizer=training_module.RMSPropOptimizer(0.1), 149 metrics=['acc']) 150 151 x = np.random.random((1, 3)) 152 y = np.random.random((1, 3)) 153 model.train_on_batch(x, y) 154 ref_y = model.predict(x) 155 156 saved_model_dir = self._save_model_dir() 157 keras_saved_model.export_saved_model(model, saved_model_dir) 158 loaded_model = keras_saved_model.load_from_saved_model(saved_model_dir) 159 loaded_model.compile( 160 loss='mse', 161 optimizer=training_module.RMSPropOptimizer(0.1), 162 metrics=['acc']) 163 y = loaded_model.predict(x) 164 self.assertAllClose(ref_y, y, atol=1e-05) 165 166 # test that new updates are the same with both models 167 x = np.random.random((1, 3)) 168 y = np.random.random((1, 3)) 169 170 ref_loss = model.train_on_batch(x, y) 171 loss = loaded_model.train_on_batch(x, y) 172 self.assertAllClose(ref_loss, loss, atol=1e-05) 173 174 ref_y = model.predict(x) 175 y = loaded_model.predict(x) 176 self.assertAllClose(ref_y, y, atol=1e-05) 177 178 # test saving/loading again 179 saved_model_dir2 = self._save_model_dir('saved_model_2') 180 keras_saved_model.export_saved_model(loaded_model, saved_model_dir2) 181 loaded_model = keras_saved_model.load_from_saved_model(saved_model_dir2) 182 y = loaded_model.predict(x) 183 self.assertAllClose(ref_y, y, atol=1e-05) 184 185 def test_saving_subclassed_model_raise_error(self): 186 # For now, saving subclassed model should raise an error. It should be 187 # avoided later with loading from SavedModel.pb. 188 189 class SubclassedModel(training.Model): 190 191 def __init__(self): 192 super(SubclassedModel, self).__init__() 193 self.layer1 = keras.layers.Dense(3) 194 self.layer2 = keras.layers.Dense(1) 195 196 def call(self, inp): 197 return self.layer2(self.layer1(inp)) 198 199 model = SubclassedModel() 200 201 saved_model_dir = self._save_model_dir() 202 with self.assertRaises(NotImplementedError): 203 keras_saved_model.export_saved_model(model, saved_model_dir) 204 205 206 class LayerWithLearningPhase(keras.engine.base_layer.Layer): 207 208 def call(self, x): 209 phase = keras.backend.learning_phase() 210 output = tf_utils.smart_cond( 211 phase, lambda: x * 0, lambda: array_ops.identity(x)) 212 if not context.executing_eagerly(): 213 output._uses_learning_phase = True # pylint: disable=protected-access 214 return output 215 216 def compute_output_shape(self, input_shape): 217 return input_shape 218 219 220 def functional_model(uses_learning_phase=True): 221 inputs = keras.layers.Input(shape=(3,)) 222 x = keras.layers.Dense(2)(inputs) 223 x = keras.layers.Dense(3)(x) 224 if uses_learning_phase: 225 x = LayerWithLearningPhase()(x) 226 return keras.models.Model(inputs, x) 227 228 229 def sequential_model(uses_learning_phase=True): 230 model = keras.models.Sequential() 231 model.add(keras.layers.Dense(2, input_shape=(3,))) 232 model.add(keras.layers.Dense(3)) 233 if uses_learning_phase: 234 model.add(LayerWithLearningPhase()) 235 return model 236 237 238 def sequential_model_without_input_shape(uses_learning_phase=True): 239 model = keras.models.Sequential() 240 model.add(keras.layers.Dense(2)) 241 model.add(keras.layers.Dense(3)) 242 if uses_learning_phase: 243 model.add(LayerWithLearningPhase()) 244 return model 245 246 247 class Subclassed(keras.models.Model): 248 249 def __init__(self): 250 super(Subclassed, self).__init__() 251 self.dense1 = keras.layers.Dense(2) 252 self.dense2 = keras.layers.Dense(3) 253 254 def call(self, inputs): 255 x = self.dense1(inputs) 256 x = self.dense2(x) 257 return x 258 259 260 def subclassed_model(): 261 return Subclassed() 262 263 264 def load_model(sess, path, mode): 265 tags = model_utils.EXPORT_TAG_MAP[mode] 266 sig_def_key = model_utils.SIGNATURE_KEY_MAP[mode] 267 268 meta_graph_def = loader_impl.load(sess, tags, path) 269 inputs = { 270 k: sess.graph.get_tensor_by_name(v.name) 271 for k, v in meta_graph_def.signature_def[sig_def_key].inputs.items()} 272 outputs = { 273 k: sess.graph.get_tensor_by_name(v.name) 274 for k, v in meta_graph_def.signature_def[sig_def_key].outputs.items()} 275 return inputs, outputs, meta_graph_def 276 277 278 @test_util.run_all_in_graph_and_eager_modes 279 class TestModelSavedModelExport(test.TestCase, parameterized.TestCase): 280 281 def _save_model_dir(self, dirname='saved_model'): 282 temp_dir = self.get_temp_dir() 283 self.addCleanup(shutil.rmtree, temp_dir, ignore_errors=True) 284 return os.path.join(temp_dir, dirname) 285 286 @parameterized.parameters( 287 { 288 'model_builder': functional_model, 289 'uses_learning_phase': True, 290 'optimizer_cls': adadelta.Adadelta, 291 'train_before_export': True}, 292 { 293 'model_builder': functional_model, 294 'uses_learning_phase': True, 295 'optimizer_cls': training_module.AdadeltaOptimizer, 296 'train_before_export': False}, 297 { 298 'model_builder': functional_model, 299 'uses_learning_phase': False, 300 'optimizer_cls': None, 301 'train_before_export': False}, 302 { 303 'model_builder': sequential_model, 304 'uses_learning_phase': True, 305 'optimizer_cls': training_module.AdadeltaOptimizer, 306 'train_before_export': True}, 307 { 308 'model_builder': sequential_model, 309 'uses_learning_phase': True, 310 'optimizer_cls': adadelta.Adadelta, 311 'train_before_export': False}, 312 { 313 'model_builder': sequential_model, 314 'uses_learning_phase': False, 315 'optimizer_cls': None, 316 'train_before_export': False}, 317 { 318 'model_builder': sequential_model_without_input_shape, 319 'uses_learning_phase': True, 320 'optimizer_cls': training_module.AdadeltaOptimizer, 321 'train_before_export': False}) 322 def testSaveAndLoadSavedModelExport( 323 self, model_builder, uses_learning_phase, optimizer_cls, 324 train_before_export): 325 optimizer = None if optimizer_cls is None else optimizer_cls() 326 327 saved_model_dir = self._save_model_dir() 328 329 np.random.seed(130) 330 input_arr = np.random.random((1, 3)) 331 target_arr = np.random.random((1, 3)) 332 333 model = model_builder(uses_learning_phase) 334 if optimizer is not None: 335 model.compile( 336 loss='mse', 337 optimizer=optimizer, 338 metrics=['mae']) 339 if train_before_export: 340 model.train_on_batch(input_arr, target_arr) 341 342 ref_loss, ref_mae = model.evaluate(input_arr, target_arr) 343 344 ref_predict = model.predict(input_arr) 345 346 # Export SavedModel 347 keras_saved_model.export_saved_model(model, saved_model_dir) 348 349 input_name = model.input_names[0] 350 output_name = model.output_names[0] 351 target_name = output_name + '_target' 352 353 # Load predict graph, and test predictions 354 with session.Session(graph=ops.Graph()) as sess: 355 inputs, outputs, _ = load_model(sess, saved_model_dir, 356 mode_keys.ModeKeys.PREDICT) 357 358 predictions = sess.run(outputs[output_name], 359 {inputs[input_name]: input_arr}) 360 self.assertAllClose(ref_predict, predictions, atol=1e-05) 361 362 if optimizer: 363 # Load eval graph, and test predictions, loss and metric values 364 with session.Session(graph=ops.Graph()) as sess: 365 inputs, outputs, _ = load_model(sess, saved_model_dir, 366 mode_keys.ModeKeys.TEST) 367 368 # First obtain the loss and predictions, and run the metric update op by 369 # feeding in the inputs and targets. 370 metrics_name = 'mae' if tf2.enabled() else 'mean_absolute_error' 371 metrics_update_op_key = 'metrics/' + metrics_name + '/update_op' 372 metrics_value_op_key = 'metrics/' + metrics_name + '/value' 373 374 loss, predictions, _ = sess.run( 375 (outputs['loss'], outputs['predictions/' + output_name], 376 outputs[metrics_update_op_key]), { 377 inputs[input_name]: input_arr, 378 inputs[target_name]: target_arr 379 }) 380 381 # The metric value should be run after the update op, to ensure that it 382 # reflects the correct value. 383 metric_value = sess.run(outputs[metrics_value_op_key]) 384 385 self.assertEqual(int(train_before_export), 386 sess.run(training_module.get_global_step())) 387 self.assertAllClose(ref_loss, loss, atol=1e-05) 388 self.assertAllClose(ref_mae, metric_value, atol=1e-05) 389 self.assertAllClose(ref_predict, predictions, atol=1e-05) 390 391 # Load train graph, and check for the train op, and prediction values 392 with session.Session(graph=ops.Graph()) as sess: 393 inputs, outputs, meta_graph_def = load_model( 394 sess, saved_model_dir, mode_keys.ModeKeys.TRAIN) 395 self.assertEqual(int(train_before_export), 396 sess.run(training_module.get_global_step())) 397 self.assertIn('loss', outputs) 398 self.assertIn(metrics_update_op_key, outputs) 399 self.assertIn(metrics_value_op_key, outputs) 400 self.assertIn('predictions/' + output_name, outputs) 401 402 # Train for a step 403 train_op = loader_impl.get_train_op(meta_graph_def) 404 train_outputs, _ = sess.run( 405 [outputs, train_op], {inputs[input_name]: input_arr, 406 inputs[target_name]: target_arr}) 407 self.assertEqual(int(train_before_export) + 1, 408 sess.run(training_module.get_global_step())) 409 410 if uses_learning_phase: 411 self.assertAllClose( 412 [[0, 0, 0]], train_outputs['predictions/' + output_name], 413 atol=1e-05) 414 else: 415 self.assertNotAllClose( 416 [[0, 0, 0]], train_outputs['predictions/' + output_name], 417 atol=1e-05) 418 419 def testSaveAndLoadSavedModelWithCustomObject(self): 420 saved_model_dir = self._save_model_dir() 421 with session.Session(graph=ops.Graph()) as sess: 422 def relu6(x): 423 return keras.backend.relu(x, max_value=6) 424 inputs = keras.layers.Input(shape=(1,)) 425 outputs = keras.layers.Activation(relu6)(inputs) 426 model = keras.models.Model(inputs, outputs) 427 keras_saved_model.export_saved_model( 428 model, saved_model_dir, custom_objects={'relu6': relu6}) 429 with session.Session(graph=ops.Graph()) as sess: 430 inputs, outputs, _ = load_model(sess, saved_model_dir, 431 mode_keys.ModeKeys.PREDICT) 432 input_name = model.input_names[0] 433 output_name = model.output_names[0] 434 predictions = sess.run( 435 outputs[output_name], {inputs[input_name]: [[7], [-3], [4]]}) 436 self.assertAllEqual([[6], [0], [4]], predictions) 437 438 def testAssertModelCloneSameObjectsIgnoreOptimizer(self): 439 input_arr = np.random.random((1, 3)) 440 target_arr = np.random.random((1, 3)) 441 442 model_graph = ops.Graph() 443 clone_graph = ops.Graph() 444 445 # Create two models with the same layers but different optimizers. 446 with session.Session(graph=model_graph): 447 inputs = keras.layers.Input(shape=(3,)) 448 x = keras.layers.Dense(2)(inputs) 449 x = keras.layers.Dense(3)(x) 450 model = keras.models.Model(inputs, x) 451 452 model.compile(loss='mse', optimizer=training_module.AdadeltaOptimizer()) 453 model.train_on_batch(input_arr, target_arr) 454 455 with session.Session(graph=clone_graph): 456 inputs = keras.layers.Input(shape=(3,)) 457 x = keras.layers.Dense(2)(inputs) 458 x = keras.layers.Dense(3)(x) 459 clone = keras.models.Model(inputs, x) 460 clone.compile(loss='mse', optimizer=keras.optimizers.RMSprop(lr=0.0001)) 461 clone.train_on_batch(input_arr, target_arr) 462 463 keras_saved_model._assert_same_non_optimizer_objects( 464 model, model_graph, clone, clone_graph) 465 466 def testAssertModelCloneSameObjectsThrowError(self): 467 input_arr = np.random.random((1, 3)) 468 target_arr = np.random.random((1, 3)) 469 470 model_graph = ops.Graph() 471 clone_graph = ops.Graph() 472 473 # Create two models with the same layers but different optimizers. 474 with session.Session(graph=model_graph): 475 inputs = keras.layers.Input(shape=(3,)) 476 x = keras.layers.Dense(2)(inputs) 477 x = keras.layers.Dense(3)(x) 478 model = keras.models.Model(inputs, x) 479 480 model.compile(loss='mse', optimizer=training_module.AdadeltaOptimizer()) 481 model.train_on_batch(input_arr, target_arr) 482 483 with session.Session(graph=clone_graph): 484 inputs = keras.layers.Input(shape=(3,)) 485 x = keras.layers.Dense(2)(inputs) 486 x = keras.layers.Dense(4)(x) 487 x = keras.layers.Dense(3)(x) 488 clone = keras.models.Model(inputs, x) 489 clone.compile(loss='mse', optimizer=keras.optimizers.RMSprop(lr=0.0001)) 490 clone.train_on_batch(input_arr, target_arr) 491 492 def testSaveSequentialModelWithoutInputShapes(self): 493 model = sequential_model_without_input_shape(True) 494 # A Sequential model that hasn't been built should raise an error. 495 with self.assertRaisesRegexp(ValueError, 'Please build the model'): 496 keras_saved_model.export_saved_model(model, '') 497 498 saved_model_dir = self._save_model_dir() 499 keras_saved_model.export_saved_model( 500 model, 501 saved_model_dir, 502 input_signature=tensor_spec.TensorSpec( 503 shape=(10, 11, 12, 13, 14), dtype=dtypes.float32, 504 name='spec_input')) 505 506 with session.Session(graph=ops.Graph()) as sess: 507 inputs, outputs, _ = load_model(sess, saved_model_dir, 508 mode_keys.ModeKeys.PREDICT) 509 self.assertEqual(5, inputs[next(iter(inputs.keys()))].shape.ndims) 510 self.assertEqual(5, outputs[next(iter(outputs.keys()))].shape.ndims) 511 self.assertEqual(3, outputs[next(iter(outputs.keys()))].shape[-1]) 512 513 @parameterized.parameters( 514 { 515 'model_builder': sequential_model_without_input_shape, 516 'input_signature': [tensor_spec.TensorSpec(shape=[None, 3], 517 dtype=dtypes.float32)]}, 518 { 519 'model_builder': subclassed_model, 520 'input_signature': [tensor_spec.TensorSpec(shape=[None, 3], 521 dtype=dtypes.float32)]}) 522 def testServingOnly(self, model_builder, input_signature): 523 if context.executing_eagerly(): 524 saved_model_dir = self._save_model_dir() 525 input_arr = np.random.random((5, 3)).astype(np.float32) 526 model = model_builder() 527 ref_predict = model.predict(input_arr) 528 529 keras_saved_model.export_saved_model( 530 model, 531 saved_model_dir, 532 serving_only=True, 533 input_signature=input_signature) 534 535 # Load predict graph, and test predictions 536 with session.Session(graph=ops.Graph()) as sess: 537 inputs, outputs, _ = load_model(sess, saved_model_dir, 538 mode_keys.ModeKeys.PREDICT) 539 predictions = sess.run(outputs[next(iter(outputs.keys()))], 540 {inputs[next(iter(inputs.keys()))]: input_arr}) 541 self.assertAllClose(ref_predict, predictions, atol=1e-05) 542 543 544 if __name__ == '__main__': 545 test.main() 546