Skip to main content
AI Development Toolkit

TensorFlow: Google's Machine Learning Framework

Understand what graph execution buys you, use the Keras layers that most work happens at, and pick the deployment target — server, browser, phone, or microcontroller — that actually drives the choice.

Intermediate16 minBy ToolDix Editorial

Learning objectives

  • Explain what tf.function does and when the graph helps
  • Choose the right Keras abstraction level for a model
  • Match a deployment target to the TensorFlow tool that serves it
  • Build an input pipeline that does not starve the accelerator

ToolDix original visual

AI Development practice loop
1

Frame

Name the outcome and constraints.

2

Build

Try one bounded workflow.

3

Review

Keep evidence, revise, and share.

TensorFlow is an end-to-end platform: define a model, train it, export it, and run it on a server, in a browser, on a phone, or on a microcontroller. That last part is the honest reason most teams choose it. On modelling capability the frameworks have converged; on breadth of deployment targets TensorFlow still has the widest coverage, and if your target is a browser or an embedded device the decision is largely made for you.

Eager code, graph execution

ToolDix original diagram
What tf.function does at trace time versus every call
Runs once, at trace time
print(), Python counters, list appends, data-dependent if on a tensor value
Runs on every call
Tensor ops, tf.print, tf.Variable updates, tf.cond branches
Triggers a fresh trace
New input shape or dtype -- varying shapes retrace forever and end up slower than eager
Prevents retracing
Fixed batch shapes, or an explicit input_signature on the decorator
What the graph buys
Fused ops, reused memory, distribution across GPUs or TPUs, no interpreter in the loop
The Python body describes the graph; it is not the thing being executed. That single distinction explains most tf.function surprises.

TensorFlow runs eagerly by default, like ordinary Python. tf.function traces a function once and turns it into a graph that can be optimised — operations fused, memory reused, work distributed across GPUs or TPUs — and executed without the Python interpreter in the loop.

@tf.function
def train_step(x, y):
    with tf.GradientTape() as tape:
        predictions = model(x, training=True)
        loss = loss_fn(y, predictions)
    grads = tape.gradient(loss, model.trainable_variables)
    optimizer.apply_gradients(zip(grads, model.trainable_variables))
    return loss

Tracing has one consequence worth internalising: the Python code runs at trace time, not at every call. A print() inside a traced function fires once; a Python counter increments once; a data-dependent if on a tensor value gets baked into the graph as whichever branch was taken during tracing. Use tf.print for logging inside a graph, tf.Variable for state, and tf.cond for tensor-dependent branching.

The second consequence is retracing. A new input shape or dtype triggers a fresh trace, and a function called with varying shapes will retrace continually, which is slower than eager execution. Fixed batch shapes, or an explicit input_signature, prevent it.

Keras at three levels

Most TensorFlow work now happens through Keras, which offers three abstraction levels, and knowing which you need saves considerable time.

Sequential — a linear stack of layers. Fine for straightforward feedforward and convolutional models, and unable to express anything with branching.

Functional — layers as callables composed into a graph. Handles multiple inputs, multiple outputs, shared layers, and skip connections. This covers the large majority of real architectures and is where you should default.

Subclassing — write a Model subclass with your own call method. Full Python flexibility for genuinely dynamic architectures, at the cost of losing some static-graph checking.

model.fit() is more capable than it first appears — callbacks give you early stopping, checkpointing, learning-rate scheduling, and TensorBoard logging without a custom loop. Write a custom training loop when you need something fit genuinely cannot express, such as multiple optimisers or an adversarial setup, not as a default.

The input pipeline decides your throughput

A common and expensive surprise: GPU utilisation sits at thirty percent and the bottleneck is data loading, not the model. tf.data exists to fix this, and three calls do most of the work.

dataset = (
    tf.data.Dataset.from_tensor_slices((paths, labels))
    .shuffle(10_000)
    .map(load_and_augment, num_parallel_calls=tf.data.AUTOTUNE)
    .batch(64)
    .prefetch(tf.data.AUTOTUNE)
)

num_parallel_calls parallelises preprocessing across CPU cores. prefetch overlaps the preparation of batch n+1 with the training of batch n, so the accelerator never waits. cache() is worth adding after map when the preprocessed data fits in memory and the augmentation is deterministic. Order matters: shuffle before batch, prefetch last.

Match the tool to the target

ToolDix original diagram
Pick the target before you finish the model
Server -- SavedModel + TF Serving
Versioning, request batching and hot model swaps behind a stable endpoint. The default for anything with a backend.
Browser -- TensorFlow.js
Runs client-side, so data never leaves the device and inference costs you nothing. Constrained by download size.
Mobile -- LiteRT
Converts and quantises for phones. Int8 typically cuts size around fourfold with an accuracy cost you must measure, not assume.
Microcontroller
Kilobytes of memory on embedded hardware. Architecture choices are severely constrained -- decide this first or not at all.
This breadth is the honest reason to choose TensorFlow. A model designed without its conversion constraints often cannot be converted, and the rework is real.

This is where TensorFlow earns its complexity.

Server-side inference — SavedModel plus TensorFlow Serving, which handles versioning, batching of concurrent requests, and hot model swaps behind a stable endpoint.

Browser — TensorFlow.js runs a converted model in the client, so data never leaves the device and there is no inference cost to you. The constraint is model size over the network.

Mobile — LiteRT (formerly TensorFlow Lite) converts and quantises for phones. Post-training quantisation to int8 typically shrinks a model roughly fourfold and speeds it up substantially, usually with a small accuracy cost that you must measure rather than assume.

Microcontrollers — the same lineage runs in kilobytes of memory on embedded hardware.

Decide the target before you finish the model. A model designed without its deployment constraint in mind frequently cannot be converted, or converts into something too large or too slow for the device, and the rework is real.

Common mistakes

Python side effects inside tf.function. They run once at trace time and then never again.

Retracing on every call. Varying input shapes make graph mode slower than eager.

Blaming the model for low GPU utilisation. Check the input pipeline first; prefetch and parallel map fix most of it.

Choosing the deployment target last. Conversion constraints should shape the architecture, not surprise it.

Sources and license context

These references informed the lesson. ToolDix adds its own explanation, workflow, and practice rather than reproducing source material. Every link below leaves ToolDix and opens the publisher's own site in a new tab.

Keep going

Read these next on ToolDix.

Original lessons that build on what you just read.