Warmup Guide — TensorFlow
Zero-to-expert primer for Phase 04: the TF/Keras ecosystem as a production CV engineer meets it — the Keras model-building spectrum, tf.data input pipelines, and the TFLite path to edge deployment.
Table of Contents
- Chapter 1: Why Learn Both Frameworks
- Chapter 2: The Keras API Spectrum
- Chapter 3: Eager, tf.function, and Tracing — the One Deep Difference
- Chapter 4: tf.data — Input Pipelines as Dataflow Graphs
- Chapter 5: Training with Keras — fit() and Custom Loops
- Chapter 6: SavedModel — the Deployment Contract
- Chapter 7: TFLite — the Edge Path
- Lab Walkthrough Guidance
- Success Criteria
- Interview Q&A
- References
Chapter 1: Why Learn Both Frameworks
Pragmatics, not ideology: research and most new training code is PyTorch; a large
share of deployed production CV — especially mobile/edge/embedded — runs TF/TFLite,
and teams maintain TF estates that need extending. The job market rewards bilinguals,
and the translation exercise itself deepens both: every concept in this phase maps to
a Phase 03 concept (Keras Layer ↔ nn.Module, tf.data ↔ DataLoader,
SavedModel ↔ TorchScript/ONNX export), and the one genuinely different idea —
graph tracing (Ch. 3) — is a concept PyTorch later adopted in its own form
(torch.compile). Learn TF as "same physics, different conventions, one new idea."
Chapter 2: The Keras API Spectrum
Three ways to define a model, in increasing flexibility — choosing the lowest sufficient tier is the Keras idiom:
- Sequential: a linear stack. Fine for toy models; outgrown immediately by anything with skips or multiple inputs.
- Functional (the lab's focus, and the production default): build a DAG by calling
layers on symbolic tensors:
Because the graph is explicit data, Keras can validate shapes at construction time,inputs = keras.Input(shape=(224, 224, 3)) x = layers.Conv2D(64, 3, padding="same")(inputs) x = layers.add([x, shortcut]) # skips, multi-branch — all natural model = keras.Model(inputs, outputs)model.summary()andplot_modelrender it, and serialization is exact — the payoff of declaring structure rather than executing it. - Subclassing (
keras.Modelwithcall()): PyTorch-style imperative freedom; costs you construction-time shape checking and easy graph introspection. Use when control flow demands it. - Mental model for the spectrum: Functional = "the model is a data structure" (inspectable, exportable); Subclassing = "the model is code" (flexible, opaque). TF tooling (export, TFLite conversion, pruning) prefers data structures — another reason Functional is the deployment-minded default. Note TF is NHWC (channels-last) by default — the opposite of PyTorch; conversion bugs love this.
Chapter 3: Eager, tf.function, and Tracing — the One Deep Difference
TF2 executes eagerly by default (PyTorch-like). Performance comes from
@tf.function: the first call traces the Python function — executes it with
symbolic tensors, recording ops into a graph — and subsequent calls run the compiled
graph, skipping Python entirely.
The three working rules (each a classic bug when violated):
- Python side effects run at trace time only: a
print()fires once;tf.printfires every call. A Python counter increments once. If you need stateful behavior, usetf.Variable. - Retracing triggers: new input shapes or dtypes, and new Python argument
values each create a new trace — passing a Python int batch size retraces per
value (use tensors or
input_signaturewithNonedims). - Data-dependent Python control flow doesn't survive tracing:
if tensor > 0:fails or bakes one branch; AutoGraph converts simple cases totf.cond/tf.while_loop, but the honest fix is tensor ops (tf.where).
This is the same capture problem the model-accuracy track studies deeply
(its Phase 01 lab-03 builds a tracing inspector; its Phase 04 covers PyTorch's
answer) — TF made you confront it a decade early. model.fit wraps its train step in
tf.function automatically, which is why custom Keras code is fast by default and
why violating rule 1 inside a custom layer produces silent weirdness.
Chapter 4: tf.data — Input Pipelines as Dataflow Graphs
tf.data.Dataset is the DataLoader counterpart with a more explicit philosophy:
the pipeline is itself a graph of transformations, optimized and executed in C++:
ds = (tf.data.Dataset.from_tensor_slices(paths_labels)
.shuffle(10_000) # before batch; buffer size matters
.map(decode_and_augment, num_parallel_calls=tf.data.AUTOTUNE)
.batch(64)
.prefetch(tf.data.AUTOTUNE)) # overlap input with training — always last
The performance grammar: map parallelism for CPU-bound decode/augment;
prefetch to overlap producer and consumer (the single most important call —
omitting it serializes input and training); cache() after expensive deterministic
steps when the dataset fits memory/disk; order matters (shuffle before batch; augment
after cache). For real datasets, TFRecord files (protobuf records, sequential
reads) replace per-file opens — the same large-scale I/O lesson as every other track's
data phase: sequential, sharded, parallel. Diagnosis tooling: the TF Profiler's input-
pipeline analyzer tells you whether you're input-bound — check it before buying GPUs
(the universal lesson again).
Chapter 5: Training with Keras — fit() and Custom Loops
- The compiled path:
model.compile(optimizer, loss, metrics)+model.fit(ds, validation_data=...)— with callbacks as the extension points:ModelCheckpoint(save best),EarlyStopping(patience on val metric),ReduceLROnPlateau/LearningRateScheduler,TensorBoard. Ninety percent of training needs are a callbacks list away — resisting the urge to hand-roll what a callback does is the Keras discipline. - Custom training: override
train_step()on the model (keepsfit's machinery — callbacks, distribution — while customizing the step), or go fully manual withtf.GradientTape:
The tape is autograd made explicit and scoped — record only inside thewith tf.GradientTape() as tape: loss = loss_fn(y, model(x, training=True)) grads = tape.gradient(loss, model.trainable_variables) optimizer.apply_gradients(zip(grads, model.trainable_variables))with. Note thetraining=True/Falseargument (TF's explicit version ofmodel.train()/.eval()— BatchNorm/Dropout switch per call, and forgetting it at inference is the same bestiary bug as Phase 03's). - Transfer learning mechanics mirror Phase 03 Ch. 7:
base.trainable = Falsefor the probe stage — with the documented BN subtlety (frozen BN should run in inference mode; Keras handles it whentrainable=False, if you passtraining=Falsecorrectly — read the official transfer-learning guide's BN section once).
Chapter 6: SavedModel — the Deployment Contract
model.export(path) (or tf.saved_model.save) writes the SavedModel: the traced
computation graph(s) + weights + signatures (named typed entry points) —
self-contained, Python-free, loadable by TF Serving, TFLite converter, TF.js, and
other languages. Signatures are the API contract: input names, shapes (including
which dims are dynamic), dtypes — inspect with saved_model_cli show --all.
The discipline (identical in spirit to every export path in this curriculum —
ONNX in Phase 07, the model-accuracy track's Phase 05): export, reload, and verify
outputs match the in-memory model numerically before calling it done; bake
preprocessing into the exported graph where feasible (a Normalization/resize layer
in the model) so the train-time and serve-time transforms cannot drift —
training-serving skew, pre-empted at the artifact level (Phase 02 Ch. 8's pipeline
lesson, now for tensors).
Chapter 7: TFLite — the Edge Path
TFLite is the mobile/embedded runtime: a compact flatbuffer format + interpreter with CPU/GPU/NNAPI/EdgeTPU delegates. The conversion ladder (each rung trades accuracy risk for size/speed):
- Plain conversion (FP32): graph ops → TFLite kernels; size from graph trimming.
- Dynamic-range quantization: weights INT8, activations FP32 — ~4× smaller, modest speedup, near-zero accuracy risk; the default first rung.
- Full integer quantization: weights and activations INT8 — needs a representative dataset (a few hundred real inputs) to calibrate activation ranges; required for integer-only accelerators (EdgeTPU, many MCUs/NPUs).
- Quantization-aware training when rung 3's accuracy drop is unacceptable.
This ladder is precisely the quantization story of the model-accuracy track (its Phase 03 derives the math; its Phase 06 covers NPU deployment at depth) — at this phase's level, the required competencies are: run the conversion, measure the accuracy delta on your eval set per rung (never ship an unmeasured conversion), and benchmark on-device (or with the TFLite benchmark tool) rather than trusting desktop numbers. The lab's deliverable is exactly that table: size / latency / accuracy per rung.
Lab Walkthrough Guidance
Order: labs 01→03 as numbered.
- Lab 01 (Keras Functional): rebuild your Phase 03 ResNet-ish CNN functionally —
skips force the Functional idiom; use
model.summary()to verify against your PyTorch parameter count (a bilingual sanity check); train with the callbacks stack (checkpoint + early stop + TensorBoard). - Lab 02 (tf.data): build the decode→augment→batch→prefetch pipeline from image
files; benchmark with/without
prefetch,num_parallel_calls, andcache— produce the throughput table; convert to TFRecords and measure again. Demonstrate one tf.function retracing trap and its fix. - Lab 03 (TFLite edge deploy): take lab 01's model through conversion rungs 1–3 (with a representative dataset for rung 3); produce the size/latency/accuracy table; verify SavedModel reload-equivalence first (Ch. 6's ritual).
Success Criteria
You are ready for Phase 05 when you can, from memory:
- Place a modeling task on the Sequential/Functional/Subclassing spectrum with the data-structure-vs-code rationale.
- State the three tf.function rules and the bug each prevents; explain trace-time vs run-time.
- Write the canonical tf.data pipeline and justify each stage's order; name the input-bound diagnosis tool.
- Use
train_step/GradientTapeand thetraining=flag correctly. - Describe SavedModel signatures and the export-verify ritual.
- Recite the TFLite quantization ladder with each rung's requirement and risk.
Interview Q&A
Q: Your tf.data pipeline feeds a GPU at 40% utilization. Walk through the fix.
Confirm input-bound with the Profiler's input analyzer (don't guess). Then in order:
prefetch(AUTOTUNE) present and last? map parallelized (AUTOTUNE) and is decode
the hot spot (pre-resize on disk / TFRecords to kill per-file opens)? cache() after
deterministic expensive work if it fits? shuffle buffer too large (startup stall) or
batch too small? Same playbook as PyTorch DataLoader tuning — the framework changes,
the producer-consumer physics doesn't.
Q: A model behaves differently in fit() than when you call it in a loop. Name the
suspects.
training= flag (BN/Dropout modes — fit sets it true, your loop may not),
tf.function trace staleness (your loop mutated Python state that was baked at trace
time — rule 1), different input dtypes/shapes triggering retraces or silent casts,
and regularization losses (model.losses) that fit adds but a naive custom loop
forgets. The grader is listening for trace-time-vs-run-time reasoning — it's the
chapter's one deep idea.
Q: Why does full-integer TFLite need a representative dataset when dynamic-range doesn't? Weights are static — their ranges are known exactly at conversion. Activations are input-dependent — INT8 activation quantization needs calibrated ranges (scale/zero-point per tensor), which only real sample inputs can provide. Dynamic-range sidesteps it by keeping activations float at runtime. Skimping on representativeness (wrong domain, too few samples) shows up as clipped activations and accuracy loss — measure per rung, always.
References
- Keras Functional API guide and Transfer learning guide (the BN section!)
- Better performance with tf.function — the tracing rules, authoritative
- tf.data performance guide and the TF Profiler input-pipeline analyzer
- SavedModel guide and
saved_model_cli - TFLite post-training quantization
- The model-accuracy track's Phase 03 WARMUP — the quantization math under the TFLite ladder