Building a Keras model
Sequential and functional APIs, matching the output layer and loss to the task, and the from_logits setting that silently costs you accuracy.
Two ways to build
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
# Sequential: a linear stack, fine for most feed-forward models
model = keras.Sequential([
keras.Input(shape=(784,)), # declare the shape once, here
layers.Dense(128, activation="relu"),
layers.Dropout(0.2),
layers.Dense(10), # logits, no activation
])
model.summary()# Functional: branches, multiple inputs or outputs, explicit shapes
inputs = keras.Input(shape=(784,), name="pixels")
x = layers.Dense(128, activation="relu")(inputs)
x = layers.Dropout(0.2)(x)
x = layers.Dense(64, activation="relu")(x)
outputs = layers.Dense(10, name="logits")(x)
model = keras.Model(inputs=inputs, outputs=outputs, name="mlp")
keras.utils.plot_model(model, show_shapes=True) # needs graphviz- Declare
Input(shape=...)(without the batch dimension) or let the firstDenseinfer it on the first call — but an explicit shape gives you asummary()immediately. - Use the functional API as soon as the architecture is not a straight line: residual connections, two inputs, an auxiliary output.
- Prefers layers as objects:
layers.Dense(64, activation="relu")is clearer than the string shorthands in real code.
Output layer, loss and compile
model.compile(
optimizer=keras.optimizers.Adam(learning_rate=1e-3),
loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=[keras.metrics.SparseCategoricalAccuracy(name="acc")],
)
# binary classification: one output, logit, from_logits=True
# loss=keras.losses.BinaryCrossentropy(from_logits=True)
# regression: one output, linear activation, MSE or Huber
# loss=keras.losses.MeanSquaredError()| Task | Output layer | Loss |
|---|---|---|
| Binary classification | 1 unit, no activation | BinaryCrossentropy(from_logits=True) |
| Multiclass, integer labels | n units, no activation | SparseCategoricalCrossentropy(from_logits=True) |
| Multiclass, one-hot labels | n units, no activation | CategoricalCrossentropy(from_logits=True) |
| Regression | 1 unit, linear | MeanSquaredError or Huber |
| Multi-label | n units, no activation | BinaryCrossentropy(from_logits=True) |
⚠️
Keep the softmax out of the last layer and pass
from_logits=True instead. Applying activation="softmax" and using the logits loss applies the transform twice, which flattens the gradients and quietly caps your accuracy. For inference, wrap the model so the exported version still returns probabilities.Inspecting and exporting
model.summary() # layer, output shape, parameter count
model.count_params()
len(model.layers)
model.layers[0].kernel.shape # a layer's weights
# probabilities for serving: add the activation to the trained model
probabilities = keras.Sequential([
model,
layers.Activation("softmax", name="probabilities"),
])
import numpy as np
logits = model(np.zeros((4, 784), dtype="float32")) # eager call
logits.shape # (4, 10)- Roughly 80 percent of parameters typically sit in the first dense layer when the input is wide — that is where model size comes from.
model.summary()withNonein the output shape column means the batch dimension, not an error.count_params()excludes nothing: it is the number the optimiser must keep state for, so training memory is several times larger.
FAQ
Dense or Embedding for categorical input?
Use
Embedding for high-cardinality categories, especially text tokens: it learns a dense vector per id and scales far better than one-hot input into a wide dense layer.Why does compiling not raise an error about the wrong dtype?
Keras validates shapes and dtypes lazily, on the first call. A mismatch between an
int64 label tensor and a float32 output surfaces at fit time, not at compile.Related
Tensors and eager execution Training, saving and serving
Last refreshed 2026-09-18.