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 first Dense infer it on the first call — but an explicit shape gives you a summary() 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()
TaskOutput layerLoss
Binary classification1 unit, no activationBinaryCrossentropy(from_logits=True)
Multiclass, integer labelsn units, no activationSparseCategoricalCrossentropy(from_logits=True)
Multiclass, one-hot labelsn units, no activationCategoricalCrossentropy(from_logits=True)
Regression1 unit, linearMeanSquaredError or Huber
Multi-labeln units, no activationBinaryCrossentropy(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() with None in 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.

Tensors and eager execution Training, saving and serving

Last refreshed 2026-09-18.