Does a custom Keras layer need from_config if it instantiates another custom layer internally?
19:05 30 Dec 2025

I am working with Keras 3 and have two custom layers: SingleConv and DoubleConv. Both are registered using @keras.saving.register_keras_serializable.

DoubleConv does not take a layer instance as an argument. Instead, it takes basic types (int, str) and creates SingleConv instances inside its __init__.

According to the Keras documentation:

"For more complex objects such as layers or models passed to __init__, deserialization must be handled explicitly..."

Since I am passing int and str to DoubleConv, but those are used to create a custom SingleConv internally, do I need to implement from_config for DoubleConv because of its usage of SingleConv internally ?

@keras.saving.register_keras_serializable(package="MyLayers", name="DoubleConv")
class DoubleConv(keras.layers.Layer):
    """
    A module consisting of two consecutive convolution layers (e.g. BatchNorm3d+ReLU+Conv3d).
    We use (Conv3d+ReLU) by default.
    This can be changed however by providing the 'order' argument, e.g. in order
    to change to Conv3d+BatchNorm3d+ELU use order='cbe'.

    Args:
        in_channels (int): number of input channels
        out_channels (int): number of output channels
        encoder (bool): if True we're in the encoder path, otherwise we're in the decoder
        kernel_size (int or tuple): size of the convolving kernel
        order (string): determines the order of layers, e.g.
            'cr' -> conv + ReLU
            'crb' -> conv + ReLU + batchnorm
            'cl' -> conv + LeakyReLU
            'ce' -> conv + ELU
        num_groups (int): number of groups for the GroupNorm
        upscale (int): number of the convolution to upscale in encoder if DoubleConv, default: 2
        dropout_prob (float or tuple): dropout probability for each convolution, default 0.1
        is3d (bool): if True use Conv3d instead of Conv2d layers
    """
    def __init__(self, in_channels, out_channels, encoder=True, padding='same', kernel_size=3, order='cr',
                 dropout_prob=0.1, upscale=2, is3d=True, **kwargs):
        super().__init__(**kwargs)


        self.in_channels = in_channels
        self.out_channels = out_channels
        self.encoder = encoder
        self.padding = padding
        self.kernel_size = kernel_size
        self.order = order
        self.dropout_prob = dropout_prob
        self.upscale = upscale
        self.is3d = is3d

        if self.encoder:
             # we're in the encoder path
            conv1_in_channels = self.in_channels
            if upscale == 1:
                conv1_out_channels = self.out_channels
            else:
                conv1_out_channels = self.out_channels // 2
            if conv1_out_channels < self.in_channels:
                conv1_out_channels = self.in_channels

            conv2_in_channels = conv1_out_channels
            conv2_out_channels = self.out_channels

        else:
            # we're in the decoder path, decrease the number of channels in the 1st convolution
            conv1_in_channels, conv1_out_channels = self.in_channels, self.out_channels
            conv2_in_channels, conv2_out_channels = self.out_channels, self.out_channels

        # check if dropout_prob is a tuple and if so
        # split it for different dropout probabilities for each convolution.

        if isinstance(self.dropout_prob, list) or isinstance(self.dropout_prob, tuple):
            dropout_prob1 = self.dropout_prob[0]
            dropout_prob2 = self.dropout_prob[1]
        else:
            dropout_prob1 = dropout_prob2 = self.dropout_prob


        self.conv1 = SingleConv(out_channels=conv1_out_channels, padding=self.padding, kernel_size=self.kernel_size, order=self.order,
                            dropout_prob=dropout_prob1, is3d= self.is3d)
        
        self.conv2 = SingleConv( out_channels=conv2_out_channels, padding=self.padding, kernel_size=self.kernel_size, order=self.order,
                            dropout_prob=dropout_prob2, is3d=self.is3d)
        
    def call(self, inputs, training=None):
        x = self.conv1(inputs, training=training)
        return self.conv2(x, training=training)

        
    def get_config(self):
        config = super().get_config()
        # Update the config with the custom layer's parameters
        config.update(
            {
                "in_channels": self.in_channels,
                "out_channels": self.out_channels,
                "encoder": self.encoder,
                "padding": self.padding,
                "kernel_size": self.kernel_size,
                "order": self.order,
                "dropout_prob": self.dropout_prob,
                "upscale": self.upscale,
                "is3d": self.is3d,
            }
        )
        return config
python tensorflow keras