iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Subclassing

Subclassing tf.keras.Model: custom forward passes, custom training loops, and the boundaries between Sequential / Functional / Subclass.

TensorFlow — Model subclassing

EXAMPLE
import tensorflow as tf
from tensorflow.keras import layers, Model

# ===== Three ways to define a Keras model =====
# 1. Sequential   simplest; linear stack
# 2. Functional   most flexible; DAG with shared layers, multi-in/out
# 3. Subclass     full control; custom forward + training step

# ===== Subclass example =====
class MLP(Model):
    def __init__(self, hidden, num_classes):
        super().__init__()
        self.fc1 = layers.Dense(hidden, activation='relu')
        self.dropout = layers.Dropout(0.2)
        self.fc2 = layers.Dense(num_classes, activation='softmax')

    def call(self, x, training=False):
        h = self.fc1(x)
        h = self.dropout(h, training=training)
        return self.fc2(h)

model = MLP(hidden=64, num_classes=3)
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])

# Build by calling once (so it knows input shape):
import numpy as np
X = np.random.rand(100, 4); y = np.random.randint(0, 3, 100)
model.fit(X, y, epochs=2, verbose=0)

# ===== Custom training step =====
class CustomModel(Model):
    def __init__(self):
        super().__init__()
        self.dense = layers.Dense(3, activation='softmax')
    def call(self, x):
        return self.dense(x)
    def train_step(self, data):
        x, y = data
        with tf.GradientTape() as tape:
            preds = self(x, training=True)
            loss = self.compiled_loss(y, preds)
        grads = tape.gradient(loss, self.trainable_variables)
        self.optimizer.apply_gradients(zip(grads, self.trainable_variables))
        self.compiled_metrics.update_state(y, preds)
        return {m.name: m.result() for m in self.metrics}

# Now model.fit() uses your train_step but you still benefit from .fit/.evaluate.

# ===== Custom layer =====
class GatedLinear(layers.Layer):
    def __init__(self, units):
        super().__init__()
        self.units = units
    def build(self, input_shape):
        self.w = self.add_weight('w', shape=(input_shape[-1], self.units),
                                 initializer='glorot_uniform')
        self.g = self.add_weight('g', shape=(input_shape[-1], self.units),
                                 initializer='glorot_uniform')
    def call(self, x):
        return tf.nn.sigmoid(x @ self.g) * (x @ self.w)

# ===== When to pick what =====
# Sequential   simple feed-forward
# Functional   anytime there are skip connections, multi-input/output, shared layers
# Subclass     when you need custom forward logic, custom train_step, or research code

# ===== Subclassing tradeoffs =====
# Pros: full control, easy debugging, dynamic shapes
# Cons: model.summary() less informative without explicit build_input_shape;
#       saving / loading requires custom config + get_config()

# ===== get_config + from_config (for serialisation) =====
class MLP2(Model):
    def __init__(self, hidden=64, num_classes=3, **kw):
        super().__init__(**kw)
        self.hidden = hidden; self.num_classes = num_classes
        self.fc1 = layers.Dense(hidden, activation='relu')
        self.fc2 = layers.Dense(num_classes, activation='softmax')
    def call(self, x): return self.fc2(self.fc1(x))
    def get_config(self):
        return { **super().get_config(),
                 'hidden': self.hidden, 'num_classes': self.num_classes }

# ===== Patterns to internalise =====
# - Default to Functional; reach for Subclass when you need custom logic
# - Implement build() if shapes depend on input
# - Override train_step for custom losses + manual gradient logic
# - get_config + register_keras_serializable for saving custom models

# ===== Pitfalls =====
# - Forgetting training=False at inference -> Dropout / BatchNorm misbehave
# - Subclassed models without get_config -> hard to deserialise
# - Defining layers inside call() -> new layers every call, never trained
# - Custom train_step that misses metric updates -> metrics never move

Why it matters

Subclassing tf.keras.Model gives you full control: custom forward, custom train_step, custom layers. Reach for it when Sequential and Functional cannot express the model shape. Pair with get_config for clean serialisation, and remember training=False at inference.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
import tensorflow as tf
from tensorflow.keras import layers, Model
class MLP(Model):
    def __init__(self):
        super().__init__()
        self.d1 = layers.Dense(128, activation='relu')
        self.d2 = layers.Dense(10, activation='softmax')
    def call(self, x):
        return self.d2(self.d1(x))
model = MLP()
Try it Yourself »

Discussion

Loading…