Skip to main content

Module 2 — Sequential API: a first model in a few lines

The previous module showed the low-level building blocks. Keras wraps them in an interface where a complete network fits in ten lines. The sequential API is the simplest of the three, and it covers a good half of real needs.

A stack of layers, one input, one output

Sequential stacks layers in order. Each layer's output becomes the next layer's input, with no exceptions.

from tensorflow import keras
from tensorflow.keras import layers

model = keras.Sequential([
keras.Input(shape=(28, 28)),
layers.Flatten(),
layers.Dense(128, activation="relu"),
layers.Dropout(0.2),
layers.Dense(10, activation="softmax"),
])

Four decisions are already made in that block. The input shape is declared explicitly, which lets Keras build the weights immediately instead of waiting for the first batch. Flatten turns the image into a 784-value vector. The 128-unit ReLU hidden layer applies the lesson from module 2 of course 07. The softmax output over ten units matches ten mutually exclusive classes.

Always declare the input shape

Without keras.Input, the model stays unbuilt: its weights do not exist yet, model.summary() fails, and the parameter count is unknown. Keras waits for the first batch to infer dimensions. That works, but you lose immediate shape checking, which is the best moment to catch an architecture mistake.

Compiling means choosing three things

A built model does not yet know how to learn. compile attaches an optimiser, a loss and metrics.

model.compile(
optimizer=keras.optimizers.Adam(learning_rate=1e-3),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)

The loss is not a free choice: it follows from the label format and the output activation.

LabelsOutputLoss
integers, one class per examplesoftmaxsparse_categorical_crossentropy
one-hot vectorssoftmaxcategorical_crossentropy
0 or 1sigmoid, 1 unitbinary_crossentropy
several simultaneous labelssigmoid per unitbinary_crossentropy
continuous valuelinearmse or huber

Confusing the first two rows is the most common beginner mistake. It raises no exception: it produces a loss that will not go down, or that goes down nonsensically. The sparse_ prefix means "labels are integers", nothing more.

A loss and a metric do not play the same role

The loss is what the optimiser minimises; it must be differentiable. A metric exists only to inform you and may be non-differentiable — accuracy, for instance, is not. A metric passed as loss raises an error; a loss passed in metrics works but teaches nothing.

Read the summary before training

model.summary() deserves a careful look before every training run.

Layer (type)          Output Shape       Param #
=====================================================
flatten (Flatten) (None, 784) 0
dense (Dense) (None, 128) 100480
dropout (Dropout) (None, 128) 0
dense_1 (Dense) (None, 10) 1290
=====================================================
Total params: 101,770

Two checks matter. First the shapes: every Output Shape should match your intent, and the leading None should be there throughout. Then the parameter count, which you can recompute in your head: a Dense layer from 784 inputs to 128 outputs has 784×128+128=100480784 \times 128 + 128 = 100\,480 parameters, weights plus biases. A mismatch with your own arithmetic points to a badly sized layer.

Note that Flatten and Dropout have no parameters: they are transformations with nothing to learn.

Train and monitor

history = model.fit(
x_train, y_train,
epochs=20,
batch_size=32,
validation_split=0.2,
verbose=2,
)

validation_split=0.2 holds back the last twenty percent of the data, without shuffling. If your dataset is sorted by class, that validation set will contain only some of the classes and the scores will be meaningless. Shuffle beforehand, or pass an explicit validation_data.

The history object keeps per-epoch values, and it is what you plot to read the learning curves covered in module 9 of course 07:

import matplotlib.pyplot as plt

plt.plot(history.history["loss"], label="training")
plt.plot(history.history["val_loss"], label="validation")
plt.legend()

Evaluate, predict, and do not confuse the two

loss, accuracy = model.evaluate(x_test, y_test)
probabilities = model.predict(x_test)
classes = probabilities.argmax(axis=1)

evaluate needs labels and returns scores. predict does not need labels and returns the network's raw outputs — here probabilities, not classes. Forgetting the argmax is a classic source of baffling results.

One invisible but important detail: Dropout is active during fit and inactive during evaluate and predict. Keras handles that switch automatically. This is why the reported training loss can exceed the validation loss in the first few epochs without anything being wrong.

Where Sequential stops being enough

The sequential API assumes a linear chain. It becomes powerless as soon as the architecture departs from that shape:

  • two inputs of different natures, an image and a text for instance;
  • two outputs, such as classification and regression at once;
  • a branch that bypasses layers, which is the very definition of a residual connection;
  • one layer applied twice with the same weights, as in a siamese architecture.

These four cases are not exotic: they cover most modern architectures. Hence the next module.

Key takeaways

  • Sequential stacks layers in a linear chain; declaring keras.Input builds the weights immediately and surfaces shape errors right away.
  • compile sets optimiser, loss and metrics; the loss follows from the label format, and the sparse_ prefix simply means the labels are integers.
  • Re-read model.summary() before each run: shapes must match your intent, and the parameter count must reconcile with your own arithmetic.
  • predict returns probabilities, not classes; Dropout is active during training and disabled at evaluation, which explains seemingly inverted curves.

Next module: the functional API, which lifts all four limitations listed above.