TensorFlow Learning Rate Schedules

The learning rate controls how large each weight update is during training. A fixed learning rate is a compromise — high enough to learn quickly, low enough not to overshoot. Learning rate schedules change the rate dynamically during training: high at the start for fast learning, gradually decreasing for fine-grained convergence. The right schedule often provides a 2–5% accuracy improvement over a fixed learning rate with no architectural changes.

Why a Fixed Rate Is Suboptimal

Fixed learning rate journey to the minimum:

High LR (0.01):
  Loss: ╲╱╲╱╲╱╲ (bounces around, never settles at minimum)

Low LR (0.00001):
  Loss: ──────────────────────────────╲ (reaches minimum but takes forever)

Schedule (start high, end low):
  Loss: ╲───────────────────────────╲─ (fast at start, precise at end)

Cosine Decay

Cosine decay starts the learning rate at a maximum value and smoothly decreases it following a cosine curve down to a minimum value. The smooth, gradual decay helps the model converge into a sharper minimum than a step-wise decay.

import tensorflow as tf

cosine_decay = tf.keras.optimizers.schedules.CosineDecay(
    initial_learning_rate=0.001,  # Start value
    decay_steps=10000,            # Total steps to decay over
    alpha=1e-6                    # Minimum learning rate at end
)

optimizer = tf.keras.optimizers.Adam(learning_rate=cosine_decay)
model.compile(optimizer=optimizer, loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
Cosine Decay curve (decay_steps=100):

LR
0.001 │╲
      │  ╲
      │    ╲
      │      ──╲
      │          ──────╲
      │                  ─────────╲____
0.0   └──────────────────────────────── Steps
      0    25    50    75    100

Cosine Decay With Warm Restarts

Cosine decay with warm restarts periodically resets the learning rate back to its maximum value before decaying again. Each restart lets the optimizer escape local minima it may have settled into, often finding better solutions than a single long decay.

cosine_restarts = tf.keras.optimizers.schedules.CosineDecayRestarts(
    initial_learning_rate=0.001,
    first_decay_steps=1000,   # Steps in first cycle
    t_mul=2.0,                # Each cycle is 2× longer than the previous
    m_mul=0.9,                # Each restart starts at 90% of previous peak LR
    alpha=1e-6
)
LR
0.001 │╲          ╲        ╲
      │  ╲          ╲        ╲
      │    ──╲    ╱──╲    ╱───╲
      │        ──╱    ──╱      ──
0.0   └──────────────────────────── Steps
      Cycle 1  Cycle 2  Cycle 3 (longer each time)

ExponentialDecay

ExponentialDecay multiplies the learning rate by a fixed decay factor at regular intervals. This is simpler to understand but produces a less smooth decay curve than cosine.

exp_decay = tf.keras.optimizers.schedules.ExponentialDecay(
    initial_learning_rate=0.01,
    decay_steps=1000,     # Apply decay every 1000 steps
    decay_rate=0.96,      # Multiply LR by 0.96 at each decay step
    staircase=True        # True = step decay; False = continuous decay
)

# After 0 steps:    LR = 0.01
# After 1000 steps: LR = 0.01 × 0.96 = 0.0096
# After 2000 steps: LR = 0.01 × 0.96² = 0.0092
# After 10000 steps:LR = 0.01 × 0.96^10 = 0.0066

Linear Warmup + Cosine Decay

Modern training recipes for large models start with a warmup phase where the learning rate increases linearly from near-zero to its target value. This prevents the large initial weight updates that destabilize training when weights are still random. After warmup, cosine decay brings it back down.

import tensorflow as tf
import numpy as np

class WarmupCosineDecay(tf.keras.optimizers.schedules.LearningRateSchedule):
    def __init__(self, peak_lr, warmup_steps, total_steps, min_lr=1e-6):
        self.peak_lr     = peak_lr
        self.warmup_steps = warmup_steps
        self.total_steps  = total_steps
        self.min_lr       = min_lr

    def __call__(self, step):
        step = tf.cast(step, tf.float32)
        warmup_lr = self.peak_lr * (step / self.warmup_steps)

        progress    = (step - self.warmup_steps) / (self.total_steps - self.warmup_steps)
        cosine_lr   = self.min_lr + 0.5 * (self.peak_lr - self.min_lr) * \
                      (1 + tf.cos(np.pi * progress))

        return tf.where(step < self.warmup_steps, warmup_lr, cosine_lr)

# 500 warmup steps, 5000 total steps
schedule = WarmupCosineDecay(peak_lr=0.001, warmup_steps=500, total_steps=5000)
optimizer = tf.keras.optimizers.Adam(learning_rate=schedule)
Warmup + Cosine Decay:

LR
0.001 │          ╱╲
      │         ╱  ╲
      │        ╱    ╲
      │       ╱      ──╲
      │      ╱          ───────╲___
0.0   └──────────────────────────── Steps
      0    500  (warmup)  5000
            │
         Peak LR

PolynomialDecay

poly_decay = tf.keras.optimizers.schedules.PolynomialDecay(
    initial_learning_rate=0.01,
    decay_steps=5000,
    end_learning_rate=0.0001,
    power=2.0,           # Quadratic decay (power=1.0 is linear)
    cycle=False
)

Step Decay (Manual with LambdaCallback)

# Halve the learning rate every 10 epochs
def step_decay(epoch):
    initial_lr = 0.01
    drop = 0.5
    epochs_drop = 10
    lr = initial_lr * (drop ** (epoch // epochs_drop))
    return float(lr)

lr_scheduler = tf.keras.callbacks.LearningRateScheduler(step_decay, verbose=1)
model.fit(x_train, y_train, epochs=50, callbacks=[lr_scheduler])

Monitoring the Learning Rate During Training

# Log the learning rate as a metric so it appears in TensorBoard
class LRLogger(tf.keras.callbacks.Callback):
    def on_epoch_end(self, epoch, logs=None):
        lr = float(self.model.optimizer.learning_rate)
        logs['lr'] = lr
        print(f"  LR = {lr:.6f}")

model.fit(x_train, y_train,
          epochs=50,
          callbacks=[LRLogger(),
                     tf.keras.callbacks.TensorBoard('./logs')])

Schedule Comparison

Schedule            Best For
──────────────────────────────────────────────────────────────────
Fixed LR            Quick experiments, prototypes
ExponentialDecay    Simple projects; easy to tune
CosineDecay         Most classification tasks; general purpose
CosineDecayRestarts Escaping local minima; longer training runs
Warmup + Cosine     Large models, Transformers, BERT fine-tuning
PolynomialDecay     Research-standard decay for comparison papers
ReduceLROnPlateau   When you cannot predict optimal decay schedule
──────────────────────────────────────────────────────────────────

Practical Starting Recipe

For most projects, start with this recipe:
1. Use Adam optimizer
2. Initial LR = 0.001
3. Add ReduceLROnPlateau(factor=0.5, patience=5)
4. If training for many epochs, switch to CosineDecay

For fine-tuning pre-trained models:
1. Use Adam or SGD
2. Initial LR = 1e-5 (very low to protect pre-trained weights)
3. Optionally add 100-step warmup
4. CosineDecay over fine-tuning steps

Learning rate schedules are one of the highest-impact hyperparameters you can tune. The difference between a fixed learning rate and a well-designed schedule often equals the difference between a mediocre model and a state-of-the-art one, without any changes to architecture or data. The next topic covers regularization — techniques that prevent models from memorizing training data so they perform well on unseen examples.

Leave a Comment

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