Building Neural Networks with Keras (Sequential and Functional API)
You’ve already built a couple of keras.Sequentialkeras.Sequential models in this phase. That covers
a lot of ground, but not everything — what if two layers need to skip past a third,
or a model needs two inputs, or its forward pass involves a loop? Keras gives you two
more ways to describe a model, plus the tools to actually run, save, and babysit
training once the architecture is built.
Three ways to build a model
- Sequential API — a plain stack of layers, one feeding the next. Simple, but every layer must have exactly one input and one output, in a straight line.
- Functional API — you call each layer like a function on a previous layer’s output, so you can branch, merge, and reuse layers freely. Still declarative: you describe the graph first, then feed it data.
- Subclassing API — you write the forward pass yourself, in ordinary Python,
inside a
call()call()method. Imperative, and the most flexible of the three.
flowchart TB A["Sequential API
keras.Sequential([...])"] --> D["compile → fit → evaluate → predict"] B["Functional API
layer(input) chains, branches, merges"] --> D C["Subclassing API
class MyModel(keras.Model): def call(...)"] --> D
The Functional API: Wide & Deep
A Wide & Deep network (Cheng et al., 2016) sends the raw input straight to the
output through a short “wide” path, and through a stack of hidden layers via a
“deep” path, then concatenates both before the final prediction. The wide path can
memorize simple rules; the deep path can learn more complex patterns — and a plain
SequentialSequential stack can’t express this, because the input has two separate places to
go.
from tensorflow import keras
input_ = keras.layers.Input(shape=[8])
hidden1 = keras.layers.Dense(30, activation="relu")(input_)
hidden2 = keras.layers.Dense(30, activation="relu")(hidden1)
concat = keras.layers.Concatenate()([input_, hidden2])
output = keras.layers.Dense(1)(concat)
model = keras.Model(inputs=[input_], outputs=[output])
model.compile(loss="mse", optimizer="sgd")from tensorflow import keras
input_ = keras.layers.Input(shape=[8])
hidden1 = keras.layers.Dense(30, activation="relu")(input_)
hidden2 = keras.layers.Dense(30, activation="relu")(hidden1)
concat = keras.layers.Concatenate()([input_, hidden2])
output = keras.layers.Dense(1)(concat)
model = keras.Model(inputs=[input_], outputs=[output])
model.compile(loss="mse", optimizer="sgd")Notice hidden1 = keras.layers.Dense(30, activation="relu")(input_)hidden1 = keras.layers.Dense(30, activation="relu")(input_) — you create the
layer, then immediately call it like a function, passing in the previous layer’s
output. Keras is just recording how the layers connect; no data is processed yet.
That’s the “Functional” in Functional API.
You can also give a model multiple inputs — say five features through the wide path and six (possibly overlapping) through the deep path:
from tensorflow import keras
input_A = keras.layers.Input(shape=[5], name="wide_input")
input_B = keras.layers.Input(shape=[6], name="deep_input")
hidden1 = keras.layers.Dense(30, activation="relu")(input_B)
hidden2 = keras.layers.Dense(30, activation="relu")(hidden1)
concat = keras.layers.concatenate([input_A, hidden2])
output = keras.layers.Dense(1, name="output")(concat)
model = keras.Model(inputs=[input_A, input_B], outputs=[output])
model.compile(loss="mse", optimizer=keras.optimizers.SGD(learning_rate=1e-3))
# fit/evaluate/predict now take a tuple or list, one array per input
# history = model.fit((X_train_A, X_train_B), y_train, epochs=20)from tensorflow import keras
input_A = keras.layers.Input(shape=[5], name="wide_input")
input_B = keras.layers.Input(shape=[6], name="deep_input")
hidden1 = keras.layers.Dense(30, activation="relu")(input_B)
hidden2 = keras.layers.Dense(30, activation="relu")(hidden1)
concat = keras.layers.concatenate([input_A, hidden2])
output = keras.layers.Dense(1, name="output")(concat)
model = keras.Model(inputs=[input_A, input_B], outputs=[output])
model.compile(loss="mse", optimizer=keras.optimizers.SGD(learning_rate=1e-3))
# fit/evaluate/predict now take a tuple or list, one array per input
# history = model.fit((X_train_A, X_train_B), y_train, epochs=20)Multiple outputs work the same way — useful when a task naturally has more than one target, or when an auxiliary output (attached to an earlier layer) acts as a regularizer that forces the underlying layers to learn something useful on their own:
from tensorflow import keras
output = keras.layers.Dense(1, name="main_output")(concat)
aux_output = keras.layers.Dense(1, name="aux_output")(hidden2)
model = keras.Model(inputs=[input_A, input_B], outputs=[output, aux_output])
# each output needs its own loss; give the main one more weight
model.compile(loss=["mse", "mse"], loss_weights=[0.9, 0.1], optimizer="sgd")from tensorflow import keras
output = keras.layers.Dense(1, name="main_output")(concat)
aux_output = keras.layers.Dense(1, name="aux_output")(hidden2)
model = keras.Model(inputs=[input_A, input_B], outputs=[output, aux_output])
# each output needs its own loss; give the main one more weight
model.compile(loss=["mse", "mse"], loss_weights=[0.9, 0.1], optimizer="sgd")The Subclassing API
Both APIs above are declarative — the whole architecture is a static graph before
any data flows through it, which is exactly why Keras can validate shapes early,
draw the model, and save it easily. But some architectures involve loops, branching
ifif statements, or shapes that change at runtime. For that (or just a more
imperative coding style), subclass keras.Modelkeras.Model directly: create the layers in
__init____init__, and use them in call()call().
from tensorflow import keras
class WideAndDeepModel(keras.Model):
def __init__(self, units=30, activation="relu", **kwargs):
super().__init__(**kwargs)
self.hidden1 = keras.layers.Dense(units, activation=activation)
self.hidden2 = keras.layers.Dense(units, activation=activation)
self.main_output = keras.layers.Dense(1)
self.aux_output = keras.layers.Dense(1)
def call(self, inputs):
input_A, input_B = inputs
hidden1 = self.hidden1(input_B)
hidden2 = self.hidden2(hidden1)
concat = keras.layers.concatenate([input_A, hidden2])
main_output = self.main_output(concat)
aux_output = self.aux_output(hidden2)
return main_output, aux_output
model = WideAndDeepModel()
model.compile(loss=["mse", "mse"], loss_weights=[0.9, 0.1], optimizer="sgd")from tensorflow import keras
class WideAndDeepModel(keras.Model):
def __init__(self, units=30, activation="relu", **kwargs):
super().__init__(**kwargs)
self.hidden1 = keras.layers.Dense(units, activation=activation)
self.hidden2 = keras.layers.Dense(units, activation=activation)
self.main_output = keras.layers.Dense(1)
self.aux_output = keras.layers.Dense(1)
def call(self, inputs):
input_A, input_B = inputs
hidden1 = self.hidden1(input_B)
hidden2 = self.hidden2(hidden1)
concat = keras.layers.concatenate([input_A, hidden2])
main_output = self.main_output(concat)
aux_output = self.aux_output(hidden2)
return main_output, aux_output
model = WideAndDeepModel()
model.compile(loss=["mse", "mse"], loss_weights=[0.9, 0.1], optimizer="sgd")The trade-off: inside call()call() you can write ordinary Python — forfor loops, ifif
statements, anything — but Keras can no longer inspect the architecture ahead of
time. model.summary()model.summary() only lists the layers, with no information on how they
connect, and the model can’t be cloned or (fully) saved the way a Sequential or
Functional model can. Géron’s advice: unless you genuinely need that flexibility,
stick to the Sequential or Functional API.
compile, fit, evaluate, predict — same for all three
Whichever API built the model, the training loop looks identical:
from tensorflow import keras
# model = ... (built with any of the three APIs above)
model.compile(loss="sparse_categorical_crossentropy",
optimizer="sgd",
metrics=["accuracy"])
history = model.fit(X_train, y_train, epochs=10,
validation_data=(X_valid, y_valid))
model.evaluate(X_test, y_test)
y_pred = model.predict(X_new)from tensorflow import keras
# model = ... (built with any of the three APIs above)
model.compile(loss="sparse_categorical_crossentropy",
optimizer="sgd",
metrics=["accuracy"])
history = model.fit(X_train, y_train, epochs=10,
validation_data=(X_valid, y_valid))
model.evaluate(X_test, y_test)
y_pred = model.predict(X_new)history.historyhistory.history is a dictionary of per-epoch metrics (lossloss, val_lossval_loss,
accuracyaccuracy, …) — the raw data behind every learning-curve plot you’ll draw in this
module.
Saving and loading a model
For Sequential and Functional models, saving is a single call — Keras stores the architecture, every layer’s weights, and the optimizer’s state together:
from tensorflow import keras
# model.fit(...)
model.save("my_keras_model.h5")
# ...later, possibly in a different script
reloaded = keras.models.load_model("my_keras_model.h5")from tensorflow import keras
# model.fit(...)
model.save("my_keras_model.h5")
# ...later, possibly in a different script
reloaded = keras.models.load_model("my_keras_model.h5")load_model()load_model() only works for Sequential and Functional models. A Subclassing
model’s architecture lives inside call()call(), which Keras can’t serialize — use
model.save_weights(...)model.save_weights(...) / model.load_weights(...)model.load_weights(...) instead, and you’ll need to
rebuild the class yourself before restoring the weights into it.
Callbacks: checkpointing and early stopping
If training runs for hours, you don’t want to lose everything to a crash — and you
don’t want to babysit the terminal to know when to stop. The fit()fit() method’s
callbackscallbacks argument lets Keras call your code at key points during training.
from tensorflow import keras
checkpoint_cb = keras.callbacks.ModelCheckpoint(
"my_keras_model.h5", save_best_only=True)
early_stopping_cb = keras.callbacks.EarlyStopping(
patience=10, restore_best_weights=True)
history = model.fit(X_train, y_train, epochs=100,
validation_data=(X_valid, y_valid),
callbacks=[checkpoint_cb, early_stopping_cb])from tensorflow import keras
checkpoint_cb = keras.callbacks.ModelCheckpoint(
"my_keras_model.h5", save_best_only=True)
early_stopping_cb = keras.callbacks.EarlyStopping(
patience=10, restore_best_weights=True)
history = model.fit(X_train, y_train, epochs=100,
validation_data=(X_valid, y_valid),
callbacks=[checkpoint_cb, early_stopping_cb])ModelCheckpoint(save_best_only=True)ModelCheckpoint(save_best_only=True)only overwrites the saved file when validation performance improves — so the file on disk is always your best model.EarlyStopping(patience=10)EarlyStopping(patience=10)interrupts training once validation performance hasn’t improved for 10 epochs in a row, andrestore_best_weights=Truerestore_best_weights=Truerolls the in-memory model back to its best epoch automatically. Since training now stops itself, it’s safe to setepochsepochsto a very large number.
You can write your own callback too, by subclassing keras.callbacks.Callbackkeras.callbacks.Callback and
overriding hooks like on_epoch_endon_epoch_end:
from tensorflow import keras
class PrintValTrainRatioCallback(keras.callbacks.Callback):
def on_epoch_end(self, epoch, logs):
print(f"\nval/train loss ratio: {logs['val_loss'] / logs['loss']:.2f}")from tensorflow import keras
class PrintValTrainRatioCallback(keras.callbacks.Callback):
def on_epoch_end(self, epoch, logs):
print(f"\nval/train loss ratio: {logs['val_loss'] / logs['loss']:.2f}")Mini-checkpoint
You need a model with two separate inputs that get concatenated before a shared hidden layer. Which API do you reach for — and why not Sequential?
- The Functional API — Sequential only supports a single straight-line stack of layers with one input and one output; it can’t express two inputs merging.
Next
Continue to How Neural Networks Learn (Gradient-Based Optimization) to see how
backpropagation and GradientTapeGradientTape actually compute the gradients that compile()compile()
and fit()fit() use to update every weight you just wired up.
🧪 Try It Yourself
Exercise 1 – Build a Functional Wide & Deep Model
Exercise 2 – A Subclassed Model
Exercise 3 – Configure Early Stopping
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
