48

Inspired by tf.keras.Model subclassing I created custom model.
I can train it and get successfull results, but I can't save it.
I use python3.6 with tensorflow v1.10 (or v1.9)

Minimal complete code example here:

import tensorflow as tf
from tensorflow.keras.datasets import mnist


class Classifier(tf.keras.Model):
    def __init__(self):
        super().__init__(name="custom_model")

        self.batch_norm1 = tf.layers.BatchNormalization()
        self.conv1 = tf.layers.Conv2D(32, (7, 7))
        self.pool1 = tf.layers.MaxPooling2D((2, 2), (2, 2))

        self.batch_norm2 = tf.layers.BatchNormalization()
        self.conv2 = tf.layers.Conv2D(64, (5, 5))
        self.pool2 = tf.layers.MaxPooling2D((2, 2), (2, 2))

    def call(self, inputs, training=None, mask=None):
        x = self.batch_norm1(inputs)
        x = self.conv1(x)
        x = tf.nn.relu(x)
        x = self.pool1(x)

        x = self.batch_norm2(x)
        x = self.conv2(x)
        x = tf.nn.relu(x)
        x = self.pool2(x)

        return x


if __name__ == '__main__':
    (x_train, y_train), (x_test, y_test) = mnist.load_data()

    x_train = x_train.reshape(*x_train.shape, 1)[:1000]
    y_train = y_train.reshape(*y_train.shape, 1)[:1000]

    x_test = x_test.reshape(*x_test.shape, 1)
    y_test = y_test.reshape(*y_test.shape, 1)

    y_train = tf.keras.utils.to_categorical(y_train)
    y_test = tf.keras.utils.to_categorical(y_test)

    model = Classifier()

    inputs = tf.keras.Input((28, 28, 1))

    x = model(inputs)
    x = tf.keras.layers.Flatten()(x)
    x = tf.keras.layers.Dense(10, activation="sigmoid")(x)

    model = tf.keras.Model(inputs=inputs, outputs=x)
    model.compile(optimizer="adam", loss="binary_crossentropy", metrics=["accuracy"])
    model.fit(x_train, y_train, epochs=1, shuffle=True)

    model.save("./my_model")

Error message:

1000/1000 [==============================] - 1s 1ms/step - loss: 4.6037 - acc: 0.7025
Traceback (most recent call last):
  File "/home/user/Data/test/python/mnist/mnist_run.py", line 62, in <module>
    model.save("./my_model")
  File "/home/user/miniconda3/envs/ml3.6/lib/python3.6/site-packages/tensorflow/python/keras/engine/network.py", line 1278, in save
    save_model(self, filepath, overwrite, include_optimizer)
  File "/home/user/miniconda3/envs/ml3.6/lib/python3.6/site-packages/tensorflow/python/keras/engine/saving.py", line 101, in save_model
    'config': model.get_config()
  File "/home/user/miniconda3/envs/ml3.6/lib/python3.6/site-packages/tensorflow/python/keras/engine/network.py", line 1049, in get_config
    layer_config = layer.get_config()
  File "/home/user/miniconda3/envs/ml3.6/lib/python3.6/site-packages/tensorflow/python/keras/engine/network.py", line 1028, in get_config
    raise NotImplementedError
NotImplementedError

Process finished with exit code 1

I looked into the error line and found out that get_config method checks self._is_graph_network

Do anybody deal with this problem?

Thanks!

Update 1:
On the keras 2.2.2 (not tf.keras)
Found comment (for model saving)
file: keras/engine/network.py
Function: get_config

# Subclassed networks are not serializable
# (unless serialization is implemented by
# the author of the subclassed network).

So, obviously it won't work...
I wonder, why don't they point it out in the documentation (Like: "Use subclassing without ability to save!")

Update 2:
Found in keras documentation:

In subclassed models, the model's topology is defined as Python code
(rather than as a static graph of layers). That means the model's
topology cannot be inspected or serialized. As a result, the following
methods and attributes are not available for subclassed models:

model.inputs and model.outputs.
model.to_yaml() and model.to_json()
model.get_config() and model.save().

So, there is no way to save model by using subclassing.
It's possible to only use Model.save_weights()

4
  • 2
    The reason why subclassed model can not be serializable is that keras need to trace the history of each tensor so as to determine the structure of graph, every tensor should a output of tf.keras.layers.Layer, however, subclassed model contains naive tensorflow operations like tf.nn.relu in its call method, as a result, it can not be serialized
    – Jie.Zhou
    Commented Aug 12, 2018 at 8:58
  • So, what if I use only tf.keras inside? Answer: it doesn't work
    – RedEyed
    Commented Aug 12, 2018 at 9:02
  • 4
    My suggestion is that, if you real want subclassed model, then forget Model.save, use Model.save_weights to save only weights of model, the load the weights with Model.load_weights, otherwise if you still want to save whole model instead of weights only, you must follow the functional api guide of keras
    – Jie.Zhou
    Commented Aug 12, 2018 at 9:54
  • Oh, it's really helpfull. Model.save_weights works fine. Thanks you a lot!
    – RedEyed
    Commented Aug 12, 2018 at 10:02

7 Answers 7

67

TensorFlow 2.2

Thanks for @cal for noticing me that the new TensorFlow has supported saving the custom models!

By using model.save to save the whole model and by using load_model to restore previously stored subclassed model. The following code snippets describe how to implement them.

class ThreeLayerMLP(keras.Model):

  def __init__(self, name=None):
    super(ThreeLayerMLP, self).__init__(name=name)
    self.dense_1 = layers.Dense(64, activation='relu', name='dense_1')
    self.dense_2 = layers.Dense(64, activation='relu', name='dense_2')
    self.pred_layer = layers.Dense(10, name='predictions')

  def call(self, inputs):
    x = self.dense_1(inputs)
    x = self.dense_2(x)
    return self.pred_layer(x)

def get_model():
  return ThreeLayerMLP(name='3_layer_mlp')

model = get_model()
# Save the model
model.save('path_to_my_model',save_format='tf')

# Recreate the exact same model purely from the file
new_model = keras.models.load_model('path_to_my_model')

See: Save and serialize models with Keras - Part II: Saving and Loading of Subclassed Models

TensorFlow 2.0

TL;DR:

  1. do not use model.save() for custom subclass keras model;
  2. use save_weights() and load_weights() instead.

With the help of the Tensorflow Team, it turns out the best practice of saving a Custom Sub-Class Keras Model is to save its weights and load it back when needed.

The reason that we can not simply save a Keras custom subclass model is that it contains custom codes, which can not be serialized safely. However, the weights can be saved/loaded when we have the same model structure and custom codes without any problem.

There has a great tutorial written by Francois Chollet who is the author of Keras, for how to save/load Sequential/Functional/Keras/Custom Sub-Class Models in Tensorflow 2.0 in Colab at here. In Saving Subclassed Models section, it said that:

Sequential models and Functional models are datastructures that represent a DAG of layers. As such, they can be safely serialized and deserialized.

A subclassed model differs in that it's not a datastructure, it's a piece of code. The architecture of the model is defined via the body of the call method. This means that the architecture of the model cannot be safely serialized. To load a model, you'll need to have access to the code that created it (the code of the model subclass). Alternatively, you could be serializing this code as bytecode (e.g. via pickling), but that's unsafe and generally not portable.

6
  • 2
    Note that by saving weights and architecture apart from each other you are losing compilation information (optimizer and losses) which are being saved using model.save() Commented May 26, 2019 at 6:31
  • 2
    It seems you can now save the full model even with custom code according to tensorflow's documentation. Your answer could be edited :)
    – Cal
    Commented Mar 12, 2020 at 22:34
  • 1
    Additional note for TensorFlow 2.2: You have to call model._set_inputs before this works.
    – prouast
    Commented May 4, 2020 at 1:48
  • 1
    @Huan, I have saved with model.save("NameOfModel", save_format='tf'), but after loading the model with loaded_model = keras.models.load_model('./NameOfModel'), I get the ValueError Python inputs incompatible with input_signature: inputs: ( Tensor("IteratorGetNext:0", shape=(None, 2), dtype=int32)) input_signature: ( TensorSpec(shape=(None, 2), dtype=tf.int64, name='input_1'))
    – hanzgs
    Commented Jul 20, 2020 at 7:39
  • Thank you for this answer. But does it apply to RNN as well? As I cannot use save_model once I include self.cell = layers.SimpleRNNCell(10) self.rnn = layers.RNN(self.cell, return_sequences=True, return_state=False) in my use case.
    – Paco Wong
    Commented Oct 16, 2020 at 14:35
6

Tensorflow 2.1 allows to save subclassed models with SavedModel format

From my beginning using Tensorflow, i was always a fan of Model Subclass, i feel this way of build models more pythonic and collaborative friendly. But saving the model was always a point of pain with this approach.

Recently i started to update my knowledge and reach to the following information that seems to be True for Tensorflow 2.1:

Subclassed Models

I found this

Second approach is by using model.save to save whole model and by using load_model to restore previously stored subclassed model.

This last saves the model, the weight and other stuff into a SavedModel file

And by last the confirmation:

Saving custom objects: If you are using the SavedModel format, you can skip this section. The key difference between HDF5 and SavedModel is that HDF5 uses object configs to save the model architecture, while SavedModel saves the execution graph. Thus, SavedModels are able to save custom objects like subclassed models and custom layers without requiring the orginal code.

I tested this personally, and efectively, model.save() for subclassed models generate a SavedModel save. There is no more need for use model.save_weights() or related functions, they now are more for specific usecases.

This is suposed to be the end of this painful path for all of us interested in Model Subclassing.

5

This will be fixed in an upcoming release according to the 1.13 pre-release patch notes:

  • Keras & Python API:
    • Subclassed Keras models can now be saved through tf.contrib.saved_model.save_keras_model.

EDIT: It seems this is not quite as finished as the notes suggest. The docs for that function for v1.13 state:

Model limitations: - Sequential and functional models can always be saved. - Subclassed models can only be saved when serving_only=True. This is due to the current implementation copying the model in order to export the training and evaluation graphs. Because the topology of subclassed models cannot be determined, the subclassed models cannot be cloned. Subclassed models will be entirely exportable in the future.

1

I found a way to solve it. Create a new model and load the weights from the saved .h5 model. This way is not preferred, but it works with keras 2.2.4 and tensorflow 1.12.

class MyModel(keras.Model):  
     def __init__(self, inputs, *args, **kwargs):
          outputs = func(inputs)
     super(MyModel, self).__init__( inputs=inputs, outputs=outputs, *args, **kwargs)

def get_model():  
    return MyModel(inputs, *args, **kwargs)

model = get_model()
model.save(‘file_path.h5’)

model_new = get_model()  
model_new.compile(optimizer=optimizer, loss=loss, metrics=metrics) 
model_new.load_weights(‘file_path.h5’)  
model_new.evaluate(x_test, y_test, **kwargs)
0

UPDATE: Jul 20

Recently I also tried to create my subclassed layers and model. Write your own get_config() function might be difficult. So I used model.save_weights(path_to_model_weights) and model.load_weights(path_to_model_weights). When you want to load the weights, remember to create the model with the same architecture than do model.load_weights(). See the tensorflow guide for more details.

Old Answer (Still correct) Actually, tensorflow document said:

In order to save/load a model with custom-defined layers, or a subclassed model, you should overwrite the get_config and optionally from_config methods. Additionally, you should use register the custom object so that Keras is aware of it.

For example:

class Linear(keras.layers.Layer):
    def __init__(self, units=32, **kwargs):
        super(Linear, self).__init__(**kwargs)
        self.units = units

    def build(self, input_shape):
        self.w = self.add_weight(
            shape=(input_shape[-1], self.units),
            initializer="random_normal",
            trainable=True,
        )
        self.b = self.add_weight(
            shape=(self.units,), initializer="random_normal", trainable=True
        )

    def call(self, inputs):
        return tf.matmul(inputs, self.w) + self.b

    def get_config(self):
        config = super(Linear, self).get_config()
        config.update({"units": self.units})
        return config


layer = Linear(64)
config = layer.get_config()
print(config)
new_layer = Linear.from_config(config)

The output is:

{'name': 'linear_8', 'trainable': True, 'dtype': 'float32', 'units': 64}

You can play with this simple code. For example, in function "get_config()", remove config.update(), see what's going on. See this and this for more details. These are the Keras guide on tensorflow website.

1
  • The answer you wrote is for custom layers and is not applicable to subclasses models that has been asked in the question Commented Jul 27, 2021 at 9:46
-1

Actually recreating the model with

keras.models.load_model('path_to_my_model')

didn't work for me

First we have to save_weights from the built model

model.save_weights('model_weights', save_format='tf')

Then we have to initiate a new instance for the subclass Model then compile and train_on_batch with one record and load_weights of built model

loaded_model = ThreeLayerMLP(name='3_layer_mlp')
loaded_model.compile(optimizer="adam", loss="binary_crossentropy", metrics=["accuracy"])
loaded_model.train_on_batch(x_train[:1], y_train[:1])
loaded_model.load_weights('model_weights')

This work perfectly in TensorFlow==2.2.0

-2

use model.predict before tf.saved_model.save

1
  • This is not relevant to the question asked. The prediction won't do anything Commented Jul 27, 2021 at 9:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.