FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Fixes a number of usability issues with model_to_estimator, in partic… · codeplaysoftware/tensorflow@73999dc · GitHub

Repository navigation

Commit 73999dc

Browse files
authored andcommitted
Fixes a number of usability issues with model_to_estimator, in particular:
- make it possible to use a model that was compiled with a TF optimizer (do not require a Keras optimizer) - do not require input to be dict (input_fn supports plain arrays) - do not require `config` to be a RunConfig instance, can now be a dict (better UX) - make it possible to use a subclassed model (caveat: weights are not preserved, yet) - clear error message when model isn't compiled; improve various error messages PiperOrigin-RevId: 187959927
1 parent b5f9432 commit 73999dc

3 files changed

Lines changed: 374 additions & 68 deletions

File tree

‎tensorflow/python/keras/_impl/keras/estimator.py‎

Lines changed: 239 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,15 @@
2525
from tensorflow.python.estimator import estimator as estimator_lib
2626
from tensorflow.python.estimator import export as export_lib
2727
from tensorflow.python.estimator import model_fn as model_fn_lib
28+
from tensorflow.python.estimator import run_config as run_config_lib
2829
from tensorflow.python.framework import ops
2930
from tensorflow.python.framework import random_seed
3031
from tensorflow.python.framework import sparse_tensor as sparse_tensor_lib
3132
from tensorflow.python.keras._impl.keras import backend as K
3233
from tensorflow.python.keras._impl.keras import models
34+
from tensorflow.python.keras._impl.keras import optimizers
35+
from tensorflow.python.keras._impl.keras.engine.base_layer import Layer
36+
from tensorflow.python.keras._impl.keras.engine.network import Network
3337
from tensorflow.python.keras._impl.keras.utils.generic_utils import CustomObjectScope
3438
from tensorflow.python.ops import math_ops
3539
from tensorflow.python.ops import metrics as metrics_module
@@ -50,36 +54,174 @@ def _cast_tensor_to_floatx(x):
5054
return math_ops.cast(x, K.floatx())
5155

5256

53-
def _create_ordered_io(keras_model, estimator_io_dict, is_input=True):
57+
def _create_ordered_io(keras_model, estimator_io, is_input=True):
5458
"""Create a list of tensors from IO dictionary based on Keras IO order.
5559
5660
Args:
57-
keras_model: an instance of compiled keras model.
58-
estimator_io_dict: features or labels dictionary from model_fn.
61+
keras_model: An instance of compiled keras model.
62+
estimator_io: The features or labels (dict or plain array) from model_fn.
5963
is_input: True if dictionary is for inputs.
6064
6165
Returns:
62-
a list of tensors based on Keras IO order.
66+
A list of tensors based on Keras IO order.
6367
6468
Raises:
6569
ValueError: if dictionary keys cannot be found in Keras model input_names
6670
or output_names.
6771
"""
68-
if is_input:
69-
keras_io_names = keras_model.input_names
72+
if isinstance(estimator_io, (list, tuple)):
73+
# Case currently not supported by most built-in input_fn,
74+
# but it's good to have for sanity
75+
return [_cast_tensor_to_floatx(x) for x in estimator_io]
76+
elif isinstance(estimator_io, dict):
77+
if is_input:
78+
if keras_model._is_graph_network:
79+
keras_io_names = keras_model.input_names
80+
else:
81+
keras_io_names = [
82+
'input_%d' % i for i in range(1, len(estimator_io) + 1)]
83+
else:
84+
if keras_model._is_graph_network:
85+
keras_io_names = keras_model.output_names
86+
else:
87+
keras_io_names = [
88+
'output_%d' % i for i in range(1, len(estimator_io) + 1)]
89+
90+
for key in estimator_io:
91+
if key not in keras_io_names:
92+
raise ValueError(
93+
'Cannot find %s with name "%s" in Keras Model. '
94+
'It needs to match one '
95+
'of the following: %s' % ('input' if is_input else 'output', key,
96+
', '.join(keras_io_names)))
97+
tensors = [_cast_tensor_to_floatx(estimator_io[io_name])
98+
for io_name in keras_io_names]
99+
return tensors
70100
else:
71-
keras_io_names = keras_model.output_names
101+
# Plain array.
102+
return _cast_tensor_to_floatx(estimator_io)
72103

73-
for key in estimator_io_dict:
74-
if key not in keras_io_names:
75-
raise ValueError(
76-
'Cannot find %s with name "%s" in Keras Model. It needs to match '
77-
'one of the following: %s' % ('input' if is_input else 'output', key,
78-
', '.join(keras_io_names)))
79-
tensors = []
80-
for io_name in keras_io_names:
81-
tensors.append(_cast_tensor_to_floatx(estimator_io_dict[io_name]))
82-
return tensors
104+
105+
def _in_place_subclassed_model_reset(model):
106+
"""Substitute for model cloning that works for subclassed models.
107+
108+
Subclassed models cannot be cloned because their topology is not serializable.
109+
To "instantiate" an identical model in a new TF graph, we reuse the original
110+
model object, but we clear its state.
111+
112+
After calling this function on a model intance, you can use the model instance
113+
as if it were a model clone (in particular you can use it in a new graph).
114+
115+
This method clears the state of the input model. It is thus destructive.
116+
However the original state can be restored fully by calling
117+
`_in_place_subclassed_model_state_restoration`.
118+
119+
Args:
120+
model: Instance of a Keras model created via subclassing.
121+
122+
Raises:
123+
ValueError: In case the model uses a subclassed model as inner layer.
124+
"""
125+
assert not model._is_graph_network # Only makes sense for subclassed networks
126+
# Retrieve all layers tracked by the model as well as their attribute names
127+
attributes_cache = {}
128+
for name in dir(model):
129+
try:
130+
value = getattr(model, name)
131+
except (AttributeError, ValueError, TypeError):
132+
continue
133+
if isinstance(value, Layer):
134+
attributes_cache[name] = value
135+
assert value in model._layers
136+
elif isinstance(value, (list, tuple)) and name not in ('layers', '_layers'):
137+
# Handle case: list/tuple of layers (also tracked by the Network API).
138+
if value and all(isinstance(val, Layer) for val in value):
139+
raise ValueError('We do not support the use of list-of-layers '
140+
'attributes in subclassed models used with '
141+
'`model_to_estimator` at this time. Found list '
142+
'model: %s' % name)
143+
144+
# Replace layers on the model with fresh layers
145+
layers_to_names = {value: key for key, value in attributes_cache.items()}
146+
original_layers = model._layers[:]
147+
model._layers = []
148+
for layer in original_layers: # We preserve layer order.
149+
config = layer.get_config()
150+
# This will not work for nested subclassed models used as layers.
151+
# This would be theoretically possible to support, but would add complexity.
152+
# Only do it if users complain.
153+
if isinstance(layer, Network) and not layer._is_graph_network:
154+
raise ValueError('We do not support the use of nested subclassed models '
155+
'in `model_to_estimator` at this time. Found nested '
156+
'model: %s' % layer)
157+
fresh_layer = layer.__class__.from_config(config)
158+
name = layers_to_names[layer]
159+
setattr(model, name, fresh_layer)
160+
161+
# Cache original model build attributes (in addition to layers)
162+
if (not hasattr(model, '_original_attributes_cache') or
163+
model._original_attributes_cache is None):
164+
if model.built:
165+
attributes_to_cache = [
166+
'inputs',
167+
'outputs',
168+
'_feed_outputs',
169+
'_feed_output_names',
170+
'_feed_output_shapes',
171+
'_feed_loss_fns',
172+
'loss_weights_list',
173+
'targets',
174+
'_feed_targets',
175+
'sample_weight_modes',
176+
'weighted_metrics',
177+
'metrics_names',
178+
'metrics_tensors',
179+
'metrics_updates',
180+
'stateful_metric_names',
181+
'total_loss',
182+
'sample_weights',
183+
'_feed_sample_weights',
184+
'train_function',
185+
'test_function',
186+
'predict_function',
187+
'_collected_trainable_weights',
188+
'_feed_inputs',
189+
'_feed_input_names',
190+
'_feed_input_shapes',
191+
'optimizer',
192+
]
193+
for name in attributes_to_cache:
194+
attributes_cache[name] = getattr(model, name)
195+
model._original_attributes_cache = attributes_cache
196+
197+
# Reset built state
198+
model.built = False
199+
model.inputs = None
200+
model.outputs = None
201+
202+
203+
def _in_place_subclassed_model_state_restoration(model):
204+
"""Restores the original state of a model after it was "reset".
205+
206+
This undoes this action of `_in_place_subclassed_model_reset`.
207+
208+
Args:
209+
model: Instance of a Keras model created via subclassing, on which
210+
`_in_place_subclassed_model_reset` was previously called.
211+
"""
212+
assert not model._is_graph_network
213+
# Restore layers and build attributes
214+
if (hasattr(model, '_original_attributes_cache') and
215+
model._original_attributes_cache is not None):
216+
model._layers = []
217+
for name, value in model._original_attributes_cache.items():
218+
setattr(model, name, value)
219+
model._original_attributes_cache = None
220+
else:
221+
# Restore to the state of a never-called model.
222+
model.built = False
223+
model.inputs = None
224+
model.outputs = None
83225

84226

85227
def _clone_and_build_model(mode,
@@ -93,42 +235,58 @@ def _clone_and_build_model(mode,
93235
mode: training mode.
94236
keras_model: an instance of compiled keras model.
95237
custom_objects: Dictionary for custom objects.
96-
features:
97-
labels:
238+
features: Dict of tensors.
239+
labels: Dict of tensors, or single tensor instance.
98240
99241
Returns:
100242
The newly built model.
101243
"""
102244
# Set to True during training, False for inference.
103245
K.set_learning_phase(mode == model_fn_lib.ModeKeys.TRAIN)
104246

105-
# Clone keras model.
106-
input_tensors = None if features is None else _create_ordered_io(
107-
keras_model, features)
108-
if custom_objects:
109-
with CustomObjectScope(custom_objects):
247+
# Get list of inputs.
248+
if features is None:
249+
input_tensors = None
250+
else:
251+
input_tensors = _create_ordered_io(keras_model,
252+
estimator_io=features,
253+
is_input=True)
254+
# Get list of outputs.
255+
if labels is None:
256+
target_tensors = None
257+
elif isinstance(labels, dict):
258+
target_tensors = _create_ordered_io(keras_model,
259+
estimator_io=labels,
260+
is_input=False)
261+
else:
262+
target_tensors = [
263+
_cast_tensor_to_floatx(
264+
sparse_tensor_lib.convert_to_tensor_or_sparse_tensor(labels))
265+
]
266+
267+
if keras_model._is_graph_network:
268+
if custom_objects:
269+
with CustomObjectScope(custom_objects):
270+
model = models.clone_model(keras_model, input_tensors=input_tensors)
271+
else:
110272
model = models.clone_model(keras_model, input_tensors=input_tensors)
111273
else:
112-
model = models.clone_model(keras_model, input_tensors=input_tensors)
274+
model = keras_model
275+
_in_place_subclassed_model_reset(model)
276+
if input_tensors is not None:
277+
model._set_inputs(input_tensors)
113278

114279
# Compile/Build model
115-
if mode is model_fn_lib.ModeKeys.PREDICT and not model.built:
116-
model.build()
280+
if mode is model_fn_lib.ModeKeys.PREDICT:
281+
if isinstance(model, models.Sequential):
282+
model.build()
117283
else:
118-
optimizer_config = keras_model.optimizer.get_config()
119-
optimizer = keras_model.optimizer.__class__.from_config(optimizer_config)
120-
optimizer.iterations = training_util.get_or_create_global_step()
121-
122-
# Get list of outputs.
123-
if labels is None:
124-
target_tensors = None
125-
elif isinstance(labels, dict):
126-
target_tensors = _create_ordered_io(keras_model, labels, is_input=False)
284+
if isinstance(keras_model.optimizer, optimizers.TFOptimizer):
285+
optimizer = keras_model.optimizer
127286
else:
128-
target_tensors = [
129-
_cast_tensor_to_floatx(
130-
sparse_tensor_lib.convert_to_tensor_or_sparse_tensor(labels))
131-
]
287+
optimizer_config = keras_model.optimizer.get_config()
288+
optimizer = keras_model.optimizer.__class__.from_config(optimizer_config)
289+
optimizer.iterations = training_util.get_or_create_global_step()
132290

133291
model.compile(
134292
optimizer,
@@ -168,10 +326,14 @@ def model_fn(features, labels, mode):
168326

169327
# Set loss and metric only during train and evaluate.
170328
if mode is not model_fn_lib.ModeKeys.PREDICT:
171-
model._make_train_function() # pylint: disable=protected-access
329+
if mode is model_fn_lib.ModeKeys.TRAIN:
330+
model._make_train_function() # pylint: disable=protected-access
331+
else:
332+
model._make_test_function() # pylint: disable=protected-access
172333
loss = model.total_loss
173334

174335
if model.metrics:
336+
# TODO(fchollet): support stateful metrics
175337
eval_metric_ops = {}
176338
# When each metric maps to an output
177339
if isinstance(model.metrics, dict):
@@ -195,6 +357,10 @@ def model_fn(features, labels, mode):
195357
if mode is model_fn_lib.ModeKeys.TRAIN:
196358
train_op = model.train_function.updates_op
197359

360+
if not model._is_graph_network:
361+
# Reset model state to original state,
362+
# to avoid `model_fn` being destructive for the initial model argument.
363+
_in_place_subclassed_model_state_restoration(keras_model)
198364
return model_fn_lib.EstimatorSpec(
199365
mode=mode,
200366
predictions=predictions,
@@ -274,10 +440,11 @@ def model_to_estimator(keras_model=None,
274440
"""
275441
if (not keras_model) and (not keras_model_path):
276442
raise ValueError(
277-
'Either keras_model or keras_model_path needs to be provided.')
443+
'Either `keras_model` or `keras_model_path` needs to be provided.')
278444
if keras_model and keras_model_path:
279445
raise ValueError(
280-
'Please specity either keras_model or keras_model_path but not both.')
446+
'Please specity either `keras_model` or `keras_model_path`, '
447+
'but not both.')
281448

282449
if not keras_model:
283450
if keras_model_path.startswith(
@@ -288,22 +455,42 @@ def model_to_estimator(keras_model=None,
288455
logging.info('Loading models from %s', keras_model_path)
289456
keras_model = models.load_model(keras_model_path)
290457
else:
291-
logging.info('Using the Keras model from memory.')
458+
logging.info('Using the Keras model provided.')
292459
keras_model = keras_model
293460

294-
if not hasattr(keras_model, 'optimizer'):
461+
if not hasattr(keras_model, 'optimizer') or not keras_model.optimizer:
295462
raise ValueError(
296-
'Given keras model has not been compiled yet. Please compile first '
297-
'before creating the estimator.')
463+
'The given keras model has not been compiled yet. Please compile first '
464+
'before calling `model_to_estimator`.')
465+
466+
if isinstance(config, dict):
467+
config = run_config_lib.RunConfig(**config)
298468

299469
keras_model_fn = _create_keras_model_fn(keras_model, custom_objects)
300-
est = estimator_lib.Estimator(
470+
estimator = estimator_lib.Estimator(
301471
keras_model_fn, model_dir=model_dir, config=config)
472+
302473
# Pass the config into keras backend's default session.
303-
with session.Session(config=est._session_config) as sess:
474+
with session.Session(config=estimator._session_config) as sess:
304475
K.set_session(sess)
305476

306477
keras_weights = keras_model.get_weights()
307-
# TODO(yifeif): move checkpoint initialization to scaffold.init_fn
308-
_save_first_checkpoint(keras_model, est, custom_objects, keras_weights)
309-
return est
478+
if keras_model._is_graph_network:
479+
# TODO(yifeif): move checkpoint initialization to scaffold.init_fn
480+
_save_first_checkpoint(keras_model,
481+
estimator,
482+
custom_objects,
483+
keras_weights)
484+
elif keras_model.built:
485+
logging.warning('You are creating an Estimator from a Keras model '
486+
'manually subclassed from `Model`, that was '
487+
'already called on some inputs (and thus already had '
488+
'weights). We are currently unable to preserve '
489+
'the model\'s state (its weights) '
490+
'as part of the estimator '
491+
'in this case. Be warned that the estimator '
492+
'has been created using '
493+
'a freshly initialized version of your model.\n'
494+
'Note that this doesn\'t affect the state of the '
495+
'model instance you passed as `keras_model` argument.')
496+
return estimator

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL