In 07-01 we left a figure on the table — 82-85% accuracy for the category classifier — and a promise: to beat it while barely writing more code. This final project keeps that promise with the transfer learning of 05-03: instead of training a CNN from scratch, we take MobileNetV2 pretrained on ImageNet and adapt it to our problem in two stages (feature extraction and fine-tuning). By the end you will compare both approaches with numbers in hand, decide which one TecnoMarket deploys, and close the module with all five projects delivered.

Contents

  1. The brief: redo 07-01 and beat it
  2. Phase 1: data — resizing and the correct preprocess_input
  3. Phase 2: the model with a frozen MobileNetV2
  4. Phase 3: two-stage training (extraction → fine-tuning)
  5. Phase 4: comparing the curves across stages
  6. Phase 5: an honest final comparison against the 07-01 CNN
  7. Phase 6: when transfer learning pays off (the 2×2 table, revisited)
  8. Phase 7: final packaging and business decision
  9. Closing the module: the TecnoMarket portfolio

The brief: redo 07-01 and beat it

Same problem, same data, same yardstick: CIFAR-10 as the stand-in for TecnoMarket's product categories, the same frozen test set from 07-01 and the same train/validation split. Only the strategy changes: instead of learning to see from scratch with 45,000 images, we start from a network that already learned to see with 1.4 million (ImageNet) and teach it only our categories. It is the central bet of 05-03; today we measure it against a real alternative we built ourselves — the most honest comparison possible.

Quantitative goal: clearly beat the 85% of 07-01. Typical expected result: 90-93%.

Phase 1: data — resizing and the correct preprocess_input

MobileNetV2 does not accept 32×32: it was pretrained on large images and its cascade of downsamplings would leave empty feature maps. We resize to 160×160 (a compromise between fidelity and cost; 05-03). And the point that breaks the most projects: every pretrained network demands its own preprocessing. MobileNetV2 expects pixels in [-1, 1], not the /255 we used in 07-01:

import tensorflow as tf
import numpy as np

tf.random.set_seed(42)

(x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data()
x_val, y_val = x_train[45000:], y_train[45000:]      # same split as 07-01
x_train, y_train = x_train[:45000], y_train[:45000]

IMG_SIZE = 160
BATCH = 64
AUTOTUNE = tf.data.AUTOTUNE

augmentation = tf.keras.Sequential([                  # same augmentation as 07-01 (05-04)
    tf.keras.layers.RandomFlip("horizontal"),
    tf.keras.layers.RandomTranslation(0.1, 0.1),
    tf.keras.layers.RandomZoom(0.1),
])

def preprocess(x, y):
    x = tf.image.resize(tf.cast(x, tf.float32), (IMG_SIZE, IMG_SIZE))
    x = tf.keras.applications.mobilenet_v2.preprocess_input(x)  # -> [-1, 1]
    return x, y

def dataset(x, y, training=False):
    ds = tf.data.Dataset.from_tensor_slices((x, y)).map(preprocess, AUTOTUNE)
    if training:
        ds = ds.shuffle(10_000).batch(BATCH).map(
            lambda a, b: (augmentation(a, training=True), b), AUTOTUNE)
    else:
        ds = ds.batch(BATCH)
    return ds.prefetch(AUTOTUNE)

train_ds = dataset(x_train, y_train, training=True)
val_ds = dataset(x_val, y_val)
test_ds = dataset(x_test, y_test)

Using the preprocess_input from the same model family is not optional: with the /255 from 07-01, MobileNetV2 would receive inputs outside the distribution it was pretrained on and would score several points lower without raising any visible error.

Phase 2: the model with a frozen MobileNetV2

The 05-03 recipe: a pretrained base without its head (include_top=False), frozen, plus a minimal new head:

from tensorflow.keras import layers, models

base = tf.keras.applications.MobileNetV2(
    input_shape=(IMG_SIZE, IMG_SIZE, 3),
    include_top=False,           # without the 1000-class ImageNet head
    weights="imagenet",
)
base.trainable = False           # stage 1: fully frozen

inputs = layers.Input(shape=(IMG_SIZE, IMG_SIZE, 3))
x = base(inputs, training=False)     # training=False: BN in inference mode (05-03)
x = layers.GlobalAveragePooling2D()(x)
x = layers.Dropout(0.3)(x)
outputs = layers.Dense(10, activation="softmax")(x)
model = models.Model(inputs, outputs, name="tecnomarket_tl")

model.summary()
# Total: ~2.3M parameters; trainable: ~13,000 (the head only)

Note the disproportion: the base contributes ~2.26M already-learned parameters; we train only ~13,000. The training=False in the call to the base is the subtle detail from 05-03: it keeps the BatchNormalization layers in inference mode even during the later fine-tuning, protecting the ImageNet statistics.

Phase 3: two-stage training (extraction → fine-tuning)

Feature extraction stage — head only, normal lr:

model.compile(optimizer=tf.keras.optimizers.Adam(1e-3),
              loss="sparse_categorical_crossentropy", metrics=["accuracy"])

callbacks = [
    tf.keras.callbacks.EarlyStopping(monitor="val_accuracy", patience=5,
                                     restore_best_weights=True),
    tf.keras.callbacks.ModelCheckpoint("models/tl_extraction.keras",
                                       monitor="val_accuracy", save_best_only=True),
    tf.keras.callbacks.TensorBoard(log_dir="logs/tl_phase1"),
]
hist1 = model.fit(train_ds, validation_data=val_ds, epochs=15,
                  callbacks=callbacks)
# Typical result: val_accuracy ~86-88% in 8-12 epochs

Already at this stage — training 0.6% of the parameters — we beat the CNN from 07-01. ImageNet's features (edges, textures, object shapes) transfer well to CIFAR-10.

Fine-tuning stage — unfreeze the final block, low lr:

base.trainable = True
for layer in base.layers[:100]:      # MobileNetV2 has 154 layers: we release the final ~1/3
    layer.trainable = False

model.compile(optimizer=tf.keras.optimizers.Adam(1e-5),   # lr 100x lower
              loss="sparse_categorical_crossentropy", metrics=["accuracy"])

callbacks[1] = tf.keras.callbacks.ModelCheckpoint("models/tl_finetuning.keras",
               monitor="val_accuracy", save_best_only=True)
callbacks[2] = tf.keras.callbacks.TensorBoard(log_dir="logs/tl_phase2")

hist2 = model.fit(train_ds, validation_data=val_ds, epochs=15,
                  callbacks=callbacks)
# Typical result: val_accuracy ~90-93%

The two golden rules of 05-03, applied: recompile after changing trainable (otherwise the change has no effect) and a very low learning rate (1e-5): the unfrozen layers already hold valuable knowledge; a high lr would destroy it within the first iterations (so-called catastrophic forgetting). We unfreeze only the final block because the early layers (edges, colors) are universal, while the late ones are the ones worth specializing in our categories.

Phase 4: comparing the curves across stages

Concatenate both histories and plot the validation accuracy with a vertical line at the stage change:

import matplotlib.pyplot as plt

acc = hist1.history["val_accuracy"] + hist2.history["val_accuracy"]
plt.plot(acc, label="val_accuracy")
plt.axvline(len(hist1.history["val_accuracy"]) - 1, ls="--", c="gray",
            label="fine-tuning start")
plt.xlabel("epoch"); plt.legend(); plt.grid(True)

A typical reading of the curve:

  • Stage 1: a fast climb in the first epochs and a plateau around ~87% — the head squeezes out everything that frozen, generic features can give.
  • Stage change: a visible jump of 2-4 points in the first fine-tuning epochs — the final layers adapt to CIFAR textures — and a new plateau at ~91-92%.
  • If in stage 2 the accuracy drops abruptly, the lr was too high: it's the classic symptom.

Phase 5: an honest final comparison against the 07-01 CNN

Evaluate on the same frozen test set from 07-01 and put the figures side by side. Typical values (yours will vary somewhat; use the ones you actually obtain):

test_loss, test_acc = model.evaluate(test_ds)
print(f"Transfer learning test accuracy: {test_acc:.3f}")
Criterion Our own CNN (07-01) MobileNetV2 + fine-tuning
Test accuracy ~83% ~91%
Total parameters ~1.2M ~2.3M
Trained parameters ~1.2M (all) ~13K + ~0.9M (stage 2)
Epochs to the best model 40-50 10 + 8
Training time (GPU) ~20 min ~25-35 min*
Size on disk ~14 MB ~28 MB
Inference cost Lower (32×32 input) Higher (160×160 input)

* Each transfer learning epoch is more expensive (160×160 images), but far fewer are needed.

Honest conclusions: +8 accuracy points is a huge improvement (errors fall from ~17% to ~9%: nearly halved); the price is a bigger model and more expensive inference. Repeat the 07-01 confusion matrix too: you'll see that the troublesome pairs (cat↔dog) remain the worst, but with half the confusions — with the 0.80 threshold from 03-04, typical automatic coverage rises from ~70% to ~85%.

Phase 6: when transfer learning pays off (the 2×2 table, revisited)

We revisit the 2×2 table from 05-03 and place this project in it:

Data similar to ImageNet Very different data
Little data Feature extraction (head only) Careful deep fine-tuning, or look for another base
Lots of data Partial fine-tuning ← our case Consider training from scratch

CIFAR-10 falls in "lots of data (45K), similar to ImageNet (natural objects)": the partial fine-tuning cell, and the results confirm it. The from-scratch CNN of 07-01 would only have been competitive in the bottom-right cell — for instance, medical images or spectrograms with millions of examples. For TecnoMarket's real product photos (everyday objects, tens of thousands of photos), today's recipe is almost certainly the right one.

Phase 7: final packaging and business decision

Decision: TecnoMarket deploys the transfer learning model. The accuracy improvement halves the load on the review queue (03-04), which more than makes up for the higher inference cost. The 07-01 CNN stays documented as the baseline in the repository (git, 06-04) — every model in production needs a baseline to justify itself against.

Packaging per 06-05, with the preprocessing inside the model:

raw_inputs = tf.keras.layers.Input(shape=(32, 32, 3), dtype=tf.uint8)  # raw image
x = tf.cast(raw_inputs, tf.float32)
x = tf.keras.layers.Resizing(IMG_SIZE, IMG_SIZE)(x)
x = tf.keras.applications.mobilenet_v2.preprocess_input(x)
prob = model(x, training=False)
serving_model = tf.keras.Model(raw_inputs, prob)
serving_model.save("models/category_classifier_v2.keras")

The resizing and preprocess_input travel inside the .keras file: the FastAPI service from 06-05 receives the raw image and cannot get the preprocessing wrong. Before replacing v1, the 06-05 protocol: validate v2 on the frozen set, compare against v1 on the same images, deploy with a rollback path, and recalibrate the confidence threshold on validation.

Closing the module: the TecnoMarket portfolio

With this fifth project, the TecnoMarket team (that is, you) delivers its complete portfolio:

Project Business problem Main technique Status
07-01 Automatic item listing Our own CNN + threshold and review queue Documented baseline
07-02 Description drafts Character-level LSTM + temperature Prototype with human review
07-03 Fraud detection Autoencoder + reconstruction error In production with periodic recalibration
07-04 Generative creatives DCGAN with a GradientTape loop Educational prototype; production requires diffusion
07-05 Automatic listing, v2 MobileNetV2 transfer learning Deployed, replaces 07-01

Five projects, four modalities (images, text, tabular data, generation) and one common method: clear phases, honest splits, metrics fitted to the problem, humans in the loop and a packaged delivery.

Common Mistakes and Tips

  • Using /255 instead of the model family's preprocess_input: the most common silent error in transfer learning; it costs several accuracy points without throwing a single exception.
  • Forgetting to recompile after changing trainable: Keras fixes which variables get trained at compile time; without recompiling, your "fine-tuning" is still training only the head.
  • Fine-tuning with the stage 1 lr: it destroys the pretrained weights in a few iterations and the accuracy collapses. Drop the lr by two orders of magnitude.
  • Unfreezing the whole base with little data: more free parameters than your data can govern; overfitting all but guaranteed. Unfreeze by blocks, from the end backward.
  • Comparing models across different splits: the phase 5 comparison is valid only because the frozen test set and the train/val split are identical to 07-01's. Changing the data between comparisons invalidates the table.

Exercises

  1. Repeat the project training only the extraction stage (no fine-tuning) and extend the phase 5 table with three columns: our own CNN, extraction alone, extraction + fine-tuning. How much does each step contribute?
  2. Try IMG_SIZE = 96 instead of 160 and measure accuracy and time per epoch. Where does it land in the cost/benefit comparison?
  3. Unfreeze only the last 20 layers (instead of ~54) and, separately, the whole base. Compare final accuracy and curve behavior across the three unfreezing scenarios.

Solutions

  1. Just skip stage 2. Typical table: our own CNN ~83%, extraction ~87%, extraction+fine-tuning ~91%. Reading: the bulk of the jump comes from pretraining (ImageNet features); fine-tuning adds the final refinement. If the compute budget is minimal, extraction alone already beats the homemade model.
  2. At 96×96 the time per epoch roughly halves and the typical accuracy drops 1-2 points (~89-90%): less resolution, less detail for the deep layers. It is a reasonable middle ground for fast iteration, training the final version at 160.
  3. With 20 layers the result stays ~1 point below the base scenario (less adaptation capacity); with the whole base and lr=1e-5 it can match or gain a few tenths, but the curves are more fragile (higher risk of overfitting and of the initial drop) and training is slower. The final third is the robust compromise: enough adaptation without putting the universal layers at risk.

Conclusion

Final project delivered and promise kept: the v2 classifier with MobileNetV2 beats our own 07-01 CNN by about 8 points, with an honest cost comparison and a justified deployment decision. More important than the figure is the judgment you take with you: the 2×2 table for deciding when to transfer, the two stages with their two learning rates, and the discipline of always comparing against a baseline on the same frozen test set. This closes module 7: five end-to-end projects in the TecnoMarket portfolio, integrating everything learned from the first dense network of 02-05 to the deployment of 06-05. But a professional isn't done when the model works: what we have built — models that classify products, generate text and images, and flag customers as suspicious — has consequences for real people. Module 8 tackles exactly that: the ethics, impact and future of Deep Learning that every practitioner must master.

© Copyright 2026. All rights reserved