TensorFlow Fine-Tuning Layers

Fine-tuning selectively unfreezes layers in a pre-trained model and trains them on your dataset at a very low learning rate. The earlier layers — which detect edges, colors, and basic textures — are generally universal and stay frozen. The later layers — which detect domain-specific features like specific flower petal shapes or car model details — benefit from updating their weights on your data. Fine-tuning bridges the gap between pure transfer learning and training from scratch.

Two-Phase Transfer Learning Strategy

Phase 1: Feature Extraction (frozen base)
  ─ All base model layers frozen (trainable=False)
  ─ Only your custom classifier head trains
  ─ Use standard learning rate: 0.001
  ─ Train for 10–20 epochs
  ─ Goal: get the classifier head to a good starting point

Phase 2: Fine-Tuning (partially unfrozen base)
  ─ Unfreeze top N layers of the base model
  ─ Train the whole model at a much lower learning rate: 0.00001
  ─ Train for 5–15 more epochs
  ─ Goal: adapt domain-specific features to your data

Which Layers to Unfreeze

Pre-trained model layers (early → late):

Layer 1–20:  Edges, color gradients          ← Always keep FROZEN
Layer 21–50: Textures, simple patterns       ← Usually keep FROZEN
Layer 51–80: Object parts, complex features  ← Fine-tune these
Layer 81+:   High-level object understanding ← Fine-tune these

Rule of thumb: Unfreeze the top 20–40% of layers
- The more different your data from ImageNet, the more layers to unfreeze
- The smaller your dataset, the fewer layers to unfreeze

Complete Fine-Tuning Code

import tensorflow as tf

# ── Phase 1: Build model with frozen base ─────────────────────────
base = tf.keras.applications.MobileNetV2(
    input_shape=(224, 224, 3),
    include_top=False,
    weights='imagenet'
)
base.trainable = False   # Freeze everything

# Build the full model
inputs  = tf.keras.Input(shape=(224, 224, 3))
x = tf.keras.applications.mobilenet_v2.preprocess_input(inputs)
x = base(x, training=False)
x = tf.keras.layers.GlobalAveragePooling2D()(x)
x = tf.keras.layers.Dense(256, activation='relu')(x)
x = tf.keras.layers.Dropout(0.3)(x)
outputs = tf.keras.layers.Dense(5, activation='softmax')(x)

model = tf.keras.Model(inputs, outputs)

# Compile Phase 1
model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

print(f"Trainable layers: {len(model.trainable_variables)}")

# Train Phase 1
history1 = model.fit(train_ds, epochs=15, validation_data=val_ds)


# ── Phase 2: Unfreeze top layers and fine-tune ────────────────────
base.trainable = True

total_layers = len(base.layers)
fine_tune_from = total_layers - 30   # Unfreeze last 30 layers

print(f"Total base layers:     {total_layers}")
print(f"Unfreezing from layer: {fine_tune_from}")

# Freeze the early layers, unfreeze the rest
for layer in base.layers[:fine_tune_from]:
    layer.trainable = False
for layer in base.layers[fine_tune_from:]:
    layer.trainable = True

# Recompile with a much lower learning rate
model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5),  # 100× smaller
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

print(f"Trainable layers after unfreezing: {len(model.trainable_variables)}")

# Train Phase 2
history2 = model.fit(
    train_ds,
    epochs=10,
    validation_data=val_ds,
    callbacks=[
        tf.keras.callbacks.EarlyStopping(
            monitor='val_accuracy', patience=4, restore_best_weights=True
        )
    ]
)

Why the Learning Rate Must Be Low

High learning rate during fine-tuning:
  Pre-trained weights have carefully calibrated values.
  A large update destroys this structure in a few steps.
  Result: accuracy crashes back to random-guess level.

Low learning rate (1e-5 or smaller):
  Each update is a tiny nudge to the weights.
  The model retains its ImageNet knowledge.
  It makes small, precise adjustments for your domain.
  Result: accuracy improves steadily above Phase 1 levels.

Diagnosing Fine-Tuning Results

Good fine-tuning:
  Phase 1 best val_acc: 85%
  Phase 2 best val_acc: 91%   ← Improved by 6%

Bad fine-tuning (too high learning rate):
  Phase 1 best val_acc: 85%
  Phase 2 val_acc:      55%  ← Dropped (weights destroyed)
  Fix: reduce learning rate by 10×

Negligible improvement from fine-tuning:
  Phase 1 best val_acc: 85%
  Phase 2 best val_acc: 85.5%  ← Barely improved
  Causes: dataset too different from ImageNet,
          not enough data to update reliably,
          base features already optimal for your task

BatchNormalization Behavior During Fine-Tuning

# BatchNormalization layers behave differently during training vs inference
# During fine-tuning, always pass training=False to the base model
# This uses running statistics computed during ImageNet training
# rather than small-batch statistics from your dataset

x = base(preprocessed_inputs, training=False)  # ← Important!

# If you use training=True on a small dataset, the batch statistics
# will be noisy and overwrite the carefully calibrated running stats.

Comparing Fine-Tuning Depths

Flowers Dataset (3,670 images, 5 classes):

Strategy                    Val Accuracy
──────────────────────────────────────────────────────
Train from scratch           ~60%
Feature extraction only      ~82%
Fine-tune last 10 layers     ~86%
Fine-tune last 30 layers     ~91%
Fine-tune last 50 layers     ~90%  ← slight overfit with small dataset
Fine-tune entire model       ~85%  ← more overfitting, less improvement
──────────────────────────────────────────────────────
Optimal: fine-tune a moderate number of late layers

Saving the Fine-Tuned Model

# Save the complete model (base + custom head, fine-tuned weights)
model.save('finetuned_mobilenetv2_flowers.keras')

# Load and use
loaded = tf.keras.models.load_model('finetuned_mobilenetv2_flowers.keras')
predictions = loaded.predict(test_ds)

Fine-tuning is the technique that makes transfer learning reach its full potential. Phase 1 trains your custom head to understand your classes using fixed features. Phase 2 refines those features so they are optimally suited for your data. Together, the two phases routinely achieve 5–10% accuracy improvements over feature extraction alone. The next topic applies fine-tuning to MobileNet in a practical end-to-end project.

Leave a Comment

Your email address will not be published. Required fields are marked *