skip to content

What must a Keras custom layer's get_config return to survive a .keras round trip?

level: seniorimportance: should knowfreq 46%

answer

  1. whatever __init__ needs must be in it
  2. merge your keys over the base config
  3. omissions reload as defaults, silently
  4. nested objects need explicit serialization

basics

~10 s

get_config must return a JSON-serializable dict carrying every constructor argument, merged over super().get_config(). Anything omitted comes back as a default after reload. Nested Keras objects need keras.saving.serialize_keras_object plus a from_config override that deserializes them.

solid answer

~40 s

Keras reconstructs a layer by calling `cls.from_config(config)`, whose default implementation is `cls(**config)`. So `get_config` must return every keyword argument `__init__` needs, in JSON-serializable form, and must start from `super().get_config()` so base attributes like `name` and `dtype` survive. An argument you forget does not raise - the layer is rebuilt with the default and the model is quietly wrong. When a config value is itself a Keras object (a nested layer, an initializer, an activation object), store it with `keras.saving.serialize_keras_object` and override `from_config` to call `keras.saving.deserialize_keras_object` before constructing, because the default would pass a raw dict into `__init__`. Keras separately records a build config, so lazily created variables get their shapes back.

code

python · 20 lines
python
import keras

@keras.saving.register_keras_serializable(package="MyPkg")
class Wrapper(keras.layers.Layer):
    def __init__(self, inner, **kwargs):
        super().__init__(**kwargs)
        self.inner = inner

    def call(self, x):
        return self.inner(x)

    def get_config(self):
        config = super().get_config()
        config["inner"] = keras.saving.serialize_keras_object(self.inner)
        return config

    @classmethod
    def from_config(cls, config):
        config["inner"] = keras.saving.deserialize_keras_object(config["inner"])
        return cls(**config)

go deeper

for a junior

Know that get_config returns the arguments needed to rebuild the layer and that you merge them over super().get_config() before returning.

for a middle

Explain that from_config defaults to cls(**config), so a missing key silently reloads as a default value while the weights still load and the model looks healthy.

for a senior

Demonstrate the nested case: serialize_keras_object in get_config, a from_config override with deserialize_keras_object, and a round-trip unit test using non-default arguments.

for a principal

Push serializability into the definition of done for shared layer code, since a model that cannot be reconstructed from its own config is an operational dead end no amount of retraining fixes.

## The contract Saving a layer calls `get_config()`; loading calls `from_config(config)`. The base `from_config` is essentially `cls(**config)`. Everything follows from that: whatever `__init__` needs must be present in the dict, and every value in the dict must be JSON-serializable. ``` def get_config(self): config = super().get_config() config.update({"units": self.units, "factor": self.factor}) return config ``` Starting from `super().get_config()` is not optional politeness. The base Layer config carries `name`, `dtype` and `trainable`. Return only your own keys and those are lost: the reloaded layer gets an auto-generated name, which breaks name-based lookups such as `model.get_layer("my_name")`. ## The silent-wrong-answer Omitting an argument does not raise. A layer built as `Scale(factor=3.0)` whose `get_config` forgets `factor` reloads as `Scale()` with the default `factor=2.0`. Weights load fine, shapes match, predictions are wrong. That is the reason to unit-test the round trip: build the layer with non-default arguments, save, reload, and assert the attributes and outputs match. The opposite failure is loud: put a key in the config that `__init__` does not accept and reconstruction raises a TypeError about an unexpected keyword argument. Loud is better, but both come from the same drift between the constructor signature and the config. ## JSON-serializable, and nested objects The config becomes `config.json`, so values must be numbers, strings, booleans, lists, dicts or None. A NumPy array, a tensor or an arbitrary Python object cannot go in as-is - convert it (for example with `.tolist()`) or reconsider whether that value is a constructor argument at all rather than a weight. Nested Keras objects are the case worth rehearsing. A layer that takes another layer, an initializer or an activation object as an argument should serialize it: ``` def get_config(self): config = super().get_config() config["inner"] = keras.saving.serialize_keras_object(self.inner) return config @classmethod def from_config(cls, config): config["inner"] = keras.saving.deserialize_keras_object(config["inner"]) return cls(**config) ``` Without the `from_config` override, the default `cls(**config)` hands your constructor a plain dict where it expected a layer, and you get a confusing error deep inside the first forward pass instead of at load time. `deserialize_keras_object` also honours the custom-object registry, so a registered nested class resolves without extra arguments. ## Build state Configuration is not the whole story for lazily built layers. A layer that creates weights in `build(input_shape)` needs that shape again to create the same variables before values can be loaded. Keras handles this with a separate build config: `get_build_config()` returns the recorded input shape and `build_from_config(config)` replays it. The defaults are right for the common case; you override them only when your build depends on something the default does not capture. ## Where interviewers push Expect the follow-up: does registering the class fix a bad get_config? No - the two are orthogonal. Registration makes the class findable by name; get_config makes the instance reconstructible with the right arguments. A registered class with an incomplete config loads happily and behaves wrongly, which is the worse of the two bugs precisely because nothing complains.

  • What breaks if get_config skips super().get_config()?
    You lose the base Layer attributes carried there, notably name, dtype and trainable. The reloaded layer gets an auto-generated name, so model.get_layer('my_name') fails and any name-based handling misbehaves, and a layer you had built in a non-default dtype comes back in the default one. Always merge your keys over the base dict.
  • Why does a lazily built layer still restore its weights when the config carries no shapes?
    Because Keras stores a build config alongside the layer config. get_build_config() records the input shape seen at build time and build_from_config() replays it on load, so variables are created with the right shapes before values are streamed in. You only override those methods when your build depends on state the default does not capture.
  • How would you test that a custom layer is serializable?
    Construct it with deliberately non-default arguments, put it in a tiny model, save to .keras, reload in the same test, and assert both the attribute values and the outputs on a fixed input match. That catches the silent case where a missing config key resets an argument to its default and only the numbers change.

saying these in an interview costs you the question

  • Returning only your own keys, dropping the base config
  • Assuming a missing config key raises an error
  • Putting tensors or NumPy arrays into the config
  • Passing a nested layer through without serializing it
  • Thinking registration compensates for an incomplete config

context