| name | keras |
| description | [Applies to: **/*.py] Definitive guidelines for writing robust, maintainable, and high-performance Keras 3 models, emphasizing the functional API, modern training patterns, and multi-backend compatibility. |
| source | cursor_mdc |
keras Best Practices
Keras 3 is engineered for developer experience, emphasizing debugging speed, elegance, and deployability. Adhere to these rules for production-ready, multi-backend Keras projects.
1. Code Organization and Structure
Always separate concerns: Model definition, data pipelines, and training logic must be distinct. This improves readability, testability, and reusability.
❌ BAD: Monolithic script
import keras
import numpy as np
x_train = np.random.rand(100, 10)
y_train = np.random.randint(0, 2, (100, 1))
inputs = keras.Input(shape=(10,))
x = keras.layers.Dense(32, activation="relu")(inputs)
outputs = keras.layers.Dense(1, activation="sigmoid")(x)
model = keras.Model(inputs, outputs)
model.compile(optimizer="adam", loss="binary_crossentropy")
model.fit(x_train, y_train, epochs=10)
✅ GOOD: Modular components
import keras
def build_simple_model(input_shape=(10,)):
inputs = keras.Input(shape=input_shape, name="input_layer")
x = keras.layers.Dense(32, activation="relu", name="hidden_dense")(inputs)
outputs = keras.layers.Dense(1, activation="sigmoid", name="output_dense")(x)
model = keras.Model(inputs, outputs, name="simple_classifier")
return model
import keras
import numpy as np
from my_model import build_simple_model
x_train = np.random.rand(100, 10)
y_train = np.random.randint(0, 2, (100, 1))
model = build_simple_model()
model.compile(optimizer="adam", loss="binary_crossentropy", metrics=["accuracy"])
callbacks = [
keras.callbacks.EarlyStopping(patience=3, monitor="val_loss"),
keras.callbacks.ModelCheckpoint("best_model.keras", save_best_only=True)
]
model.fit(x_train, y_train, epochs=50, validation_split=0.2, callbacks=callbacks)
2. Common Patterns and Anti-patterns
2.1 Model Building: Functional API is King
Always use the Functional API for most models. It's explicit, easy to inspect, and handles complex topologies (multi-input/output, shared layers) gracefully. Subclassing is reserved for highly custom, non-standard layers or models with dynamic graph execution.
❌ BAD: Subclassing for simple sequential models
class MySequentialModel(keras.Model):
def __init__(self):
super().__init__()
self.dense1 = keras.layers.Dense(32, activation="relu")
self.dense2 = keras.layers.Dense(1, activation="sigmoid")
def call(self, inputs):
x = self.dense1(inputs)
return self.dense2(x)
✅ GOOD: Functional API for clarity and debuggability
inputs = keras.Input(shape=(10,), name="input_features")
x = keras.layers.Dense(32, activation="relu", name="hidden_layer")(inputs)
outputs = keras.layers.Dense(1, activation="sigmoid", name="output_prediction")(x)
model = keras.Model(inputs=inputs, outputs=outputs, name="simple_functional_model")
keras.utils.plot_model(model, "simple_functional_model.png", show_shapes=True)
2.2 Training Loops: Leverage model.fit and Callbacks
Always use model.fit() with Keras's powerful built-in Callbacks. This simplifies training, provides standard logging, and ensures best practices like early stopping and model saving are applied consistently. Only implement a custom training loop (via the "Trainer pattern") when model.fit() is insufficient for complex research or custom gradient logic.
❌ BAD: Manual training loop without callbacks
optimizer = keras.optimizers.Adam()
loss_fn = keras.losses.BinaryCrossentropy()
for epoch in range(10):
for step, (x_batch, y_batch) in enumerate(dataset):
with tf.GradientTape() as tape:
logits = model(x_batch)
loss_value = loss_fn(y_batch, logits)
grads = tape.gradient(loss_value, model.trainable_weights)
optimizer.apply_gradients(zip(grads, model.trainable_weights))
print(f"Epoch {epoch}, Loss: {loss_value.numpy()}")
✅ GOOD: model.fit() with essential callbacks
model.compile(
optimizer=keras.optimizers.Adam(learning_rate=1e-3),
loss=keras.losses.BinaryCrossentropy(),
metrics=["accuracy"]
)
callbacks = [
keras.callbacks.EarlyStopping(monitor="val_loss", patience=5, restore_best_weights=True),
keras.callbacks.ModelCheckpoint("best_model_epoch_{epoch:02d}.keras", save_best_only=True, monitor="val_loss"),
keras.callbacks.TensorBoard(log_dir="./logs")
]
model.fit(
x_train, y_train,
epochs=50,
batch_size=32,
validation_split=0.2,
callbacks=callbacks,
verbose=1
)
3. Performance Considerations
3.1 Mixed Precision Training
Always enable mixed precision for modern GPUs/TPUs. It significantly speeds up training and reduces memory usage with minimal impact on accuracy.
keras.mixed_precision.set_global_policy("mixed_float16")
inputs = keras.Input(shape=(224, 224, 3))
x = keras.layers.Conv2D(32, 3, activation="relu")(inputs)
outputs = keras.layers.Dense(10, activation="softmax", dtype="float32")(x)
model = keras.Model(inputs, outputs)
3.2 Multi-Backend Execution
Always configure your backend explicitly using the KERAS_BACKEND environment variable. This ensures consistency and allows easy switching between TensorFlow, JAX, or PyTorch.
❌ BAD: Relying on default or implicit backend
import keras
✅ GOOD: Explicit backend configuration (before import keras)
import os
os.environ["KERAS_BACKEND"] = "jax"
import keras
4. Common Pitfalls and Gotchas
4.1 Reproducibility
Always set seeds for NumPy, Python's random module, and the Keras backend for reproducible results, especially during development and experimentation.
import os
import random
import numpy as np
import tensorflow as tf
random.seed(42)
np.random.seed(42)
tf.random.set_seed(42)
os.environ['TF_DETERMINISTIC_OPS'] = '1'
os.environ['TF_CUDNN_DETERMINISTIC'] = '1'
import keras
4.2 Keras 2 vs. Keras 3
Always use import keras for Keras 3. Avoid from tensorflow import keras if you need to guarantee Keras 3 behavior, especially with older TensorFlow versions. Ensure pip install --upgrade keras is run.
❌ BAD: Potential Keras 2 import
from tensorflow import keras
✅ GOOD: Explicit Keras 3 import
import keras
5. Type Hints
Always use type hints for function signatures and class attributes. This improves code clarity, enables static analysis, and reduces bugs.
❌ BAD: Untyped function
def build_model(input_shape):
inputs = keras.Input(shape=input_shape)
x = keras.layers.Dense(10)(inputs)
return keras.Model(inputs, x)
✅ GOOD: Type-hinted function
import keras
from typing import Tuple
def build_model(input_shape: Tuple[int, ...]) -> keras.Model:
inputs = keras.Input(shape=input_shape)
x = keras.layers.Dense(10, activation="relu")(inputs)
outputs = keras.layers.Dense(1, activation="sigmoid")(x)
return keras.Model(inputs, outputs, name="typed_model")
6. Virtual Environments
Always use a virtual environment (e.g., venv, conda) for every project. This isolates dependencies and prevents conflicts.
python -m venv .venv
source .venv/bin/activate
pip install --upgrade keras tensorflow
7. Packaging
Package reusable components (custom layers, models, data utilities) into a local package. This promotes modularity and easy sharing across projects.
my_project/
├── src/
│ ├── __init__.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── custom_layers.py # Contains custom Keras layers
│ └── data/
│ ├── __init__.py
│ └── data_loader.py # Contains data loading functions
├── tests/
│ └── test_models.py
└── train.py
8. Testing Approaches
Always write unit tests for custom layers, preprocessing functions, and other standalone components. Implement integration tests for full model training and inference pipelines.
import unittest
import keras
from src.models.custom_layers import MyCustomLayer
class TestCustomLayer(unittest.TestCase):
def test_output_shape(self):
layer = MyCustomLayer(units=10)
inputs = keras.ops.zeros((1, 5))
outputs = layer(inputs)
self.assertEqual(outputs.shape, (1, 10))
def test_trainable_weights(self):
layer = MyCustomLayer(units=10)
_ = layer(keras.ops.zeros((1, 5)))
self.assertGreater(len(layer.trainable_weights), 0)
import keras
class MyCustomLayer(keras.layers.Layer):
def __init__(self, units, **kwargs):
super().__init__(**kwargs)
self.units = units
def build(self, input_shape):
self.w = self.add_weight(
shape=(input_shape[-1], self.units),
initializer="random_normal",
trainable=,
)
.b = .add_weight(
shape=(.units,), initializer=, trainable=
)
():
keras.ops.matmul(inputs, .w) + .b