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.
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.
| Labels | Output | Loss |
|---|---|---|
| integers, one class per example | softmax | sparse_categorical_crossentropy |
| one-hot vectors | softmax | categorical_crossentropy |
| 0 or 1 | sigmoid, 1 unit | binary_crossentropy |
| several simultaneous labels | sigmoid per unit | binary_crossentropy |
| continuous value | linear | mse 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.
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 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
Sequentialstacks layers in a linear chain; declaringkeras.Inputbuilds the weights immediately and surfaces shape errors right away.compilesets optimiser, loss and metrics; the loss follows from the label format, and thesparse_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. predictreturns probabilities, not classes;Dropoutis active during training and disabled at evaluation, which explains seemingly inverted curves.
Next module: the functional API, which lifts all four limitations listed above.