tensorflow
TensorFlow is Google's end-to-end machine learning framework: tensors and autodiff at the bottom, the Keras API for building and training models on top, and a deployment story (SavedModel, TensorFlow Serving, TFLite/LiteRT for mobile, TF.js for browsers) that is still its strongest card. You define models in Python, train on CPU/GPU/TPU, and export artifacts that run in production far from Python.
Still a serious, actively developed framework with the best production and on-device story, and the right call inside Google-stack shops or existing TF codebases. For new projects without those constraints, the gravity of the field is with PyTorch and JAX, and pretending otherwise wastes your time.
Use it if
- You need the deployment pipeline more than the research flexibility: SavedModel plus TF Serving, or on-device inference via the LiteRT/TFLite toolchain, is mature and battle-tested
- You work in the Google ecosystem: TPUs, Vertex AI, or an org already standardized on TFX pipelines
- You want the high-level Keras workflow (compile, fit, callbacks) where standard supervised training is a few dozen lines
- You maintain existing TensorFlow models; the 2.x API is stable and 2.21 keeps shipping regular releases
- You are doing research or following current papers: the research community has largely moved to PyTorch and JAX, so new architectures, reference implementations, and tutorials appear there first and TF ports lag or never arrive
- You care about install weight: the Linux x86_64 wheel for 2.21.0 is about 572 MB before dependencies, and the CUDA extras multiply that; tensorflow-cpu exists but is still heavy
- You want current native Windows GPU training: the pip GPU path is aimed at Linux, and Windows GPU work generally means WSL2 or the DirectML plugin rather than a plain pip install
- You just want to run or fine-tune LLMs: the modern tooling for that (HF transformers ecosystem, vLLM, llama.cpp) is overwhelmingly PyTorch-first
- You are learning ML from scratch in 2026: unless your target job or codebase is TF, starting with PyTorch gives you more transferable material
Setup reality
pip install tensorflow works cleanly on CPU. GPU is where the pain lives: you need a CUDA-enabled card and matching CUDA/cuDNN stack, and the version compatibility matrix between TF release, CUDA, cuDNN, and your driver is unforgiving; pip's cuda extra helps on Linux but Windows users are pushed toward WSL2 and macOS users toward the separate tensorflow-metal plugin. Expect a multi-hundred-megabyte download, a slow first import, and a wall of log noise on startup. Also note the ABI surface: numpy and protobuf version conflicts with the rest of your environment are a recurring theme, so use a dedicated virtualenv.
Patterns
Build and train a classifier with Kerastrain-basic-model
import tensorflow as tf
model = tf.keras.Sequential([
tf.keras.layers.Dense(128, activation='relu'),
tf.keras.layers.Dropout(0.2),
tf.keras.layers.Dense(10),
])
model.compile(
optimizer='adam',
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=['accuracy'],
)
model.fit(x_train, y_train, epochs=5, validation_split=0.1)from_logits=True with no final softmax is numerically safer; adding softmax AND from_logits=True is a classic silent accuracy killer.
Feed training with tf.datadata-pipeline
ds = (
tf.data.Dataset.from_tensor_slices((features, labels))
.shuffle(10_000)
.batch(32)
.prefetch(tf.data.AUTOTUNE)
)
model.fit(ds, epochs=5)Order matters: shuffle before batch, prefetch last; forgetting prefetch often leaves the GPU idle waiting on input.
Save and reload a modelsave-load-model
model.save('model.keras') # Keras v3 format
reloaded = tf.keras.models.load_model('model.keras')
# for TF Serving, export a SavedModel instead
model.export('serving/1')Since Keras 3 (TF 2.16+), .keras is the native format and model.export() produces the SavedModel for serving; old H5 files still load but are legacy.
Custom training step with GradientTapecustom-training-loop
optimizer = tf.keras.optimizers.Adam(1e-3)
@tf.function
def train_step(x, y):
with tf.GradientTape() as tape:
logits = model(x, training=True)
loss = loss_fn(y, logits)
grads = tape.gradient(loss, model.trainable_variables)
optimizer.apply_gradients(zip(grads, model.trainable_variables))
return lossEverything you differentiate must happen inside the tape context; pass training=True yourself, nothing does it for you here.
Verify GPU visibility and limit memory grabgpu-check
gpus = tf.config.list_physical_devices('GPU')
print(gpus)
for gpu in gpus:
tf.config.experimental.set_memory_growth(gpu, True)By default TF reserves nearly all GPU memory at startup; memory growth must be set before any op touches the GPU.
Load an image dataset from foldersimage-dataset-directory
train_ds = tf.keras.utils.image_dataset_from_directory(
'data/train',
image_size=(180, 180),
batch_size=32,
validation_split=0.2,
subset='training',
seed=42,
)Class labels come from folder names in alphabetical order; use the same seed and validation_split for the 'validation' subset call or the splits overlap.
Stop early and keep the best weightsearly-stopping-checkpoint
callbacks = [
tf.keras.callbacks.EarlyStopping(
monitor='val_loss', patience=3, restore_best_weights=True
),
tf.keras.callbacks.ModelCheckpoint(
'best.keras', monitor='val_loss', save_best_only=True
),
]
model.fit(ds, validation_data=val_ds, epochs=100, callbacks=callbacks)Without restore_best_weights=True you end training holding the last (worse) weights, not the best ones.
Fine-tune a pretrained backbonetransfer-learning
base = tf.keras.applications.EfficientNetV2B0(
include_top=False, weights='imagenet', pooling='avg'
)
base.trainable = False # phase 1: train the head only
inputs = tf.keras.Input(shape=(224, 224, 3))
x = base(inputs, training=False)
outputs = tf.keras.layers.Dense(num_classes)(x)
model = tf.keras.Model(inputs, outputs)Call the frozen base with training=False so BatchNorm stays in inference mode; skipping that quietly wrecks fine-tuning.
Compile a hot function to a graphtf-function-speedup
@tf.function
def score(x):
return tf.reduce_sum(model(x, training=False), axis=-1)
# inspect retracing problems
print(score.pretty_printed_concrete_signatures())Every new Python-side argument shape or dtype triggers a retrace; pass tensors, not Python scalars, or performance craters.
Convert a model for on-device inferenceexport-tflite
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_model = converter.convert()
open('model.tflite', 'wb').write(tflite_model)Optimize.DEFAULT enables quantization that shrinks the file but can shift accuracy; validate the converted model before shipping.
Train faster with mixed precisionmixed-precision
tf.keras.mixed_precision.set_global_policy('mixed_float16')
model = build_model()
# keep the final layer in float32 for numeric stability
outputs = tf.keras.layers.Activation('linear', dtype='float32')(x)Set the policy before building the model, and force the output layer to float32 or the loss can go NaN.
Make runs repeatablereproducibility
import tensorflow as tf
tf.keras.utils.set_random_seed(42) # python, numpy, tf seeds
tf.config.experimental.enable_op_determinism()Op determinism slows training and a few ops will raise errors because they have no deterministic implementation.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| torch | PyPI | Default choice for research, LLM work, and most new deep learning projects; largest community momentum |
| jax | PyPI | You want composable function transforms (grad, jit, vmap) and best-in-class TPU performance |
| keras | PyPI | Keras 3 standalone runs the same model code on TensorFlow, PyTorch, or JAX backends, decoupling you from this choice |
| onnxruntime | PyPI | You only need fast inference on an exported model, not a training framework |