Keras Essentials
Keras Essentials Keras is a high-level API for building and training neural networks, designed around fast iteration and readable code rather than exposing ever…
Keras Essentials
Keras is a high-level API for building and training neural networks, designed around fast iteration and readable code rather than exposing every low-level tensor operation. As of Keras 3, it's backend-agnostic — the same model code can run on TensorFlow, JAX, or PyTorch as the underlying execution engine, chosen via the KERAS_BACKEND environment variable. Most day-to-day work happens through three model-building styles (Sequential, Functional, Subclassing) that trade off simplicity against flexibility.
The Sequential API
Sequential is the simplest way to build a model: a linear stack of layers, each with exactly one input tensor and one output tensor, feeding into the next. It's the right choice for straightforward feed-forward architectures and the wrong choice the moment you need multiple inputs/outputs, shared layers, or non-linear connections (skip connections, branches) — reach for the Functional API there instead.
import keras
from keras import layers
model = keras.Sequential([
layers.Input(shape=(784,)),
layers.Dense(128, activation="relu"),
layers.Dropout(0.3),
layers.Dense(64, activation="relu"),
layers.Dense(10, activation="softmax"),
])
model.summary() # layer-by-layer shape + param count table
# Layers can also be added incrementally
model2 = keras.Sequential()
model2.add(layers.Input(shape=(784,)))
model2.add(layers.Dense(128, activation="relu"))
model2.add(layers.Dense(10, activation="softmax"))The Functional API
The Functional API treats layers as callables applied to tensors, so you can wire up any directed acyclic graph of layers: multiple inputs, multiple outputs, shared sub-networks applied to different inputs, and skip/residual connections. This is the default choice for anything beyond a plain stack — it's more explicit than Sequential without the overhead of a full subclassed model.
import keras
from keras import layers
# Two inputs (title text + numeric metadata) merged into one output
title_input = keras.Input(shape=(100,), name="title")
meta_input = keras.Input(shape=(10,), name="metadata")
title_features = layers.Embedding(10000, 64)(title_input)
title_features = layers.LSTM(32)(title_features)
merged = layers.concatenate([title_features, meta_input])
x = layers.Dense(64, activation="relu")(merged)
priority = layers.Dense(1, activation="sigmoid", name="priority")(x)
model = keras.Model(inputs=[title_input, meta_input], outputs=[priority])
# Residual/skip connection — impossible to express with Sequential
inputs = keras.Input(shape=(32, 32, 3))
x = layers.Conv2D(32, 3, activation="relu", padding="same")(inputs)
residual = x
x = layers.Conv2D(32, 3, activation="relu", padding="same")(x)
x = layers.add([x, residual]) # skip connectionModel Subclassing
Subclassing keras.Model and defining call() gives you a fully imperative model — arbitrary Python control flow (loops, conditionals) inside the forward pass. It's the most flexible option and the right tool for dynamic architectures (recursive networks, models whose structure depends on the input), but you lose some of what Sequential/Functional give for free: automatic shape inference, plotting the model graph, and some serialization convenience.
import keras
from keras import layers
class MyBlock(keras.Model):
def __init__(self, units=64):
super().__init__()
self.dense1 = layers.Dense(units, activation="relu")
self.dropout = layers.Dropout(0.3)
self.dense2 = layers.Dense(10, activation="softmax")
def call(self, inputs, training=False):
x = self.dense1(inputs)
# training-only behavior, and arbitrary Python control flow, are both fine here
if training:
x = self.dropout(x, training=training)
return self.dense2(x)
model = MyBlock()
model.build(input_shape=(None, 784)) # or call once on real data to build lazilyCompiling & Training
compile() attaches an optimizer, loss function, and metrics to a model; fit() then runs the training loop for a given number of epochs, batching your data automatically. This is the "just works" path for standard supervised training — most of what looks like Keras boilerplate is really just naming these three things correctly for your problem (e.g. categorical_crossentropy for one-hot labels vs. sparse_categorical_crossentropy for integer labels is a very common mismatch).
model.compile(
optimizer=keras.optimizers.Adam(learning_rate=1e-3),
loss="sparse_categorical_crossentropy", # integer labels, e.g. 3 not [0,0,0,1,...]
metrics=["accuracy"],
)
history = model.fit(
x_train, y_train,
batch_size=32,
epochs=20,
validation_split=0.2, # or pass validation_data=(x_val, y_val)
callbacks=[
keras.callbacks.EarlyStopping(monitor="val_loss", patience=3, restore_best_weights=True),
keras.callbacks.ModelCheckpoint("best_model.keras", save_best_only=True),
],
)
test_loss, test_acc = model.evaluate(x_test, y_test)
predictions = model.predict(x_test)
# history.history is a plain dict — {'loss': [...], 'val_loss': [...], ...}
import matplotlib.pyplot as plt
plt.plot(history.history["loss"], label="train")
plt.plot(history.history["val_loss"], label="val")
plt.legend()Custom Training Loops
When fit() isn't flexible enough — custom losses that need extra state, GANs with alternating generator/discriminator updates, non-standard gradient handling — you can override train_step() (keeping fit()'s callbacks, progress bar, and distribution support) or drop to a fully manual loop with a gradient tape. The manual loop is the escape hatch of last resort; override train_step() first if you still want to use fit().
import keras
import tensorflow as tf # train_step below assumes the TensorFlow backend
class CustomModel(keras.Model):
def train_step(self, data):
x, y = data
with tf.GradientTape() as tape:
y_pred = self(x, training=True)
loss = self.compute_loss(y=y, y_pred=y_pred)
trainable_vars = self.trainable_variables
gradients = tape.gradient(loss, trainable_vars)
self.optimizer.apply_gradients(zip(gradients, trainable_vars))
for metric in self.metrics:
metric.update_state(y, y_pred)
return {m.name: m.result() for m in self.metrics}
# model.fit() still works unchanged — it now calls YOUR train_step each batch,
# keeping the progress bar, callbacks, and distribution strategy support
# Fully manual loop — full control, none of fit()'s conveniences
optimizer = keras.optimizers.Adam()
loss_fn = keras.losses.SparseCategoricalCrossentropy()
for epoch in range(epochs):
for x_batch, y_batch in train_dataset:
with tf.GradientTape() as tape:
logits = model(x_batch, training=True)
loss_value = loss_fn(y_batch, logits)
grads = tape.gradient(loss_value, model.trainable_weights)
optimizer.apply_gradients(zip(grads, model.trainable_weights))Regularization & Overfitting
A shrinking training loss with a rising validation loss is the classic overfitting signature — the model is memorizing training examples instead of learning generalizable patterns. Keras exposes several standard countermeasures as ordinary layers or callbacks, so fixing overfitting rarely means changing your training loop, just your architecture and callback list.
from keras import layers, regularizers
model = keras.Sequential([
layers.Input(shape=(784,)),
layers.Dense(
128, activation="relu",
kernel_regularizer=regularizers.l2(1e-4), # penalize large weights
),
layers.BatchNormalization(), # stabilizes/speeds up training
layers.Dropout(0.5), # randomly zero activations during training
layers.Dense(10, activation="softmax"),
])
# EarlyStopping is often the single highest-leverage fix: stop once val_loss
# stops improving instead of training until it visibly overfits
callbacks = [
keras.callbacks.EarlyStopping(monitor="val_loss", patience=5, restore_best_weights=True),
keras.callbacks.ReduceLROnPlateau(monitor="val_loss", factor=0.5, patience=2),
]Gotchas & Tips
sparse_categorical_crossentropy expects integer class labels (3); categorical_crossentropy expects one-hot vectors ([0,0,0,1,...]). Mismatching the loss to your label encoding is one of the most common Keras errors, and it often fails silently with nonsensical accuracy rather than a clear error.
Layers like Dropout and BatchNormalization behave differently during training vs. inference — always call the model as model(x, training=False) (or use predict()/evaluate(), which set this automatically) when you're not training, or you'll get dropout noise or wrong batch stats in your predictions.
A subclassed model has no defined input shape until you either call build() explicitly or run data through it once — model.summary() and plotting the model will fail before that with an unbuilt model.
Save full models with the .keras format (model.save("model.keras")), not the legacy HDF5 .h5 format — .keras reliably captures custom layers/losses registered via @keras.saving.register_keras_serializable, which .h5 often does not.
validation_split only works on the last fraction of the passed array — if your data isn't already shuffled, you can end up validating on a biased slice (e.g. all of one class). Shuffle before calling fit(), or pass a pre-split, pre-shuffled validation_data instead.
Keras 3 code that avoids TensorFlow-specific calls (using keras.ops instead of tf.* directly) can run unmodified on the JAX or PyTorch backends — useful for compatibility, but any custom training loop written directly against tf.GradientTape is TensorFlow-only.