DEEP LEARNING LESSON 8 OPTIMIZERS

Optimizers With Python

In this lesson, we will use optimizers in Python and see how SGD, Momentum, and Adam are configured in a neural network. We will also understand exactly what happens when the model calls the optimizer during training.

Where Does the Optimizer Fit?

Remember the training process:

Input
  ↓
Forward Propagation
  ↓
Prediction
  ↓
Loss
  ↓
Backpropagation
  ↓
Gradients
  ↓
Optimizer
  ↓
Update Weights
  ↓
Next Training Step

The optimizer is responsible for using the gradients to update the neural network's weights.

In TensorFlow/Keras, we usually specify the optimizer when calling model.compile().

Start With a Simple Neural Network

First, create a simple neural network:

import tensorflow as tf

model = tf.keras.Sequential([
    tf.keras.layers.Dense(
        8,
        activation="relu",
        input_shape=(2,)
    ),

    tf.keras.layers.Dense(
        1,
        activation="sigmoid"
    )
])

This model has:

Input
  ↓
Hidden Layer
8 neurons
ReLU
  ↓
Output Layer
1 neuron
Sigmoid

Now we need to tell the model how its weights should be updated. That is where the optimizer comes in.

Using Adam

Adam is one of the most commonly used optimizers for neural networks.

optimizer = tf.keras.optimizers.Adam(
    learning_rate=0.001
)

Then pass the optimizer to compile():

model.compile(
    optimizer=optimizer,
    loss="binary_crossentropy",
    metrics=["accuracy"]
)

You can also write it directly:

model.compile(
    optimizer=tf.keras.optimizers.Adam(
        learning_rate=0.001
    ),
    loss="binary_crossentropy",
    metrics=["accuracy"]
)

Both approaches do the same thing.

Training the Model With Adam

Suppose we have binary classification data:

X_train = [
    [0, 0],
    [0, 1],
    [1, 0],
    [1, 1]
]

y_train = [
    0,
    1,
    1,
    1
]

We can train the model:

model.fit(
    X_train,
    y_train,
    epochs=100
)

During each training step, Keras approximately performs:

Prediction
    ↓
Calculate Loss
    ↓
Calculate Gradients
    ↓
Adam uses Gradients
    ↓
Update Weights
    ↓
Repeat

Using SGD

SGD is another optimizer.

optimizer = tf.keras.optimizers.SGD(
    learning_rate=0.01
)

Then:

model.compile(
    optimizer=optimizer,
    loss="binary_crossentropy",
    metrics=["accuracy"]
)

The important part is:

learning_rate=0.01

The learning rate controls how large the weight updates are.

Small learning rate
→ Small weight updates


Large learning rate
→ Large weight updates

Using SGD With Momentum

SGD can also use momentum.

optimizer = tf.keras.optimizers.SGD(
    learning_rate=0.01,
    momentum=0.9
)

Here:

learning_rate=0.01
→ Controls update size


momentum=0.9
→ Uses information from previous updates

So the optimizer can continue moving in a useful direction instead of treating every gradient completely independently.

Compare the Optimizers

# Adam

optimizer = tf.keras.optimizers.Adam(
    learning_rate=0.001
)


# SGD

optimizer = tf.keras.optimizers.SGD(
    learning_rate=0.01
)


# SGD + Momentum

optimizer = tf.keras.optimizers.SGD(
    learning_rate=0.01,
    momentum=0.9
)

Notice something important: the neural network itself does not have to change.

Same Model
    +
Different Optimizer
    ↓
Different Training Behavior

Complete Example With Adam

import tensorflow as tf

# Training data
X_train = tf.constant([
    [0.0, 0.0],
    [0.0, 1.0],
    [1.0, 0.0],
    [1.0, 1.0]
])

y_train = tf.constant([
    0.0,
    1.0,
    1.0,
    1.0
])


# Create model
model = tf.keras.Sequential([
    tf.keras.layers.Dense(
        8,
        activation="relu",
        input_shape=(2,)
    ),

    tf.keras.layers.Dense(
        1,
        activation="sigmoid"
    )
])


# Create optimizer
optimizer = tf.keras.optimizers.Adam(
    learning_rate=0.001
)


# Compile model
model.compile(
    optimizer=optimizer,
    loss="binary_crossentropy",
    metrics=["accuracy"]
)


# Train model
history = model.fit(
    X_train,
    y_train,
    epochs=100,
    verbose=0
)


# Make prediction
prediction = model.predict(
    [[1.0, 1.0]],
    verbose=0
)

print(prediction)

The important optimizer part is:

optimizer = tf.keras.optimizers.Adam(
    learning_rate=0.001
)

And then:

model.compile(
    optimizer=optimizer,
    loss="binary_crossentropy",
    metrics=["accuracy"]
)

What Happens During model.fit()?

When you run:

model.fit(
    X_train,
    y_train,
    epochs=100
)

Keras performs the training process repeatedly.

Epoch
  ↓
Take training data
  ↓
Forward Pass
  ↓
Prediction
  ↓
Calculate Loss
  ↓
Backpropagation
  ↓
Calculate Gradients
  ↓
Optimizer
  ↓
Update Weights
  ↓
Next Batch
  ↓
Repeat

The optimizer is therefore not responsible for the entire training process. It specifically handles the weight update step.

Understanding Learning Rate in Python

Consider two Adam optimizers:

optimizer1 = tf.keras.optimizers.Adam(
    learning_rate=0.001
)

optimizer2 = tf.keras.optimizers.Adam(
    learning_rate=0.01
)

The second optimizer has a learning rate ten times larger.

0.01 ÷ 0.001 = 10

That does not mean the model will necessarily train ten times faster. It means the optimizer is configured to take substantially larger parameter-update steps, all else equal.

If the learning rate is too large, training can become unstable.

If it is too small, training can become unnecessarily slow.

Example: Change Only the Optimizer

This is a useful experiment because everything else stays the same.

import tensorflow as tf

model = tf.keras.Sequential([
    tf.keras.layers.Dense(
        8,
        activation="relu",
        input_shape=(2,)
    ),

    tf.keras.layers.Dense(
        1,
        activation="sigmoid"
    )
])


# Try Adam
model.compile(
    optimizer=tf.keras.optimizers.Adam(
        learning_rate=0.001
    ),
    loss="binary_crossentropy",
    metrics=["accuracy"]
)

history_adam = model.fit(
    X_train,
    y_train,
    epochs=100,
    verbose=0
)

Now you could create another identical model and use SGD:

model_sgd = tf.keras.Sequential([
    tf.keras.layers.Dense(
        8,
        activation="relu",
        input_shape=(2,)
    ),

    tf.keras.layers.Dense(
        1,
        activation="sigmoid"
    )
])


model_sgd.compile(
    optimizer=tf.keras.optimizers.SGD(
        learning_rate=0.01
    ),
    loss="binary_crossentropy",
    metrics=["accuracy"]
)

history_sgd = model_sgd.fit(
    X_train,
    y_train,
    epochs=100,
    verbose=0
)

Now you can compare their training histories.

Checking the Training Loss

Keras stores the training history in the object returned by model.fit().

history = model.fit(
    X_train,
    y_train,
    epochs=100
)

print(history.history["loss"])

This gives you the loss recorded during each epoch.

Epoch 1   → loss = 0.69
Epoch 2   → loss = 0.63
Epoch 3   → loss = 0.57
...
Epoch 100 → loss = 0.08

A generally decreasing loss indicates that the model is learning from the training data, although validation performance is also important.

Using an Optimizer Manually

Normally Keras hides the gradient calculation and weight update inside model.fit().

But you can also use an optimizer directly.

import tensorflow as tf

x = tf.Variable(5.0)

optimizer = tf.keras.optimizers.SGD(
    learning_rate=0.1
)

with tf.GradientTape() as tape:

    loss = (x - 2) ** 2

gradient = tape.gradient(loss, x)

optimizer.apply_gradients([
    (gradient, x)
])

print(x.numpy())

Here, the goal is to make:

x → 2

The loss is:

loss = (x - 2)²

Since:

x = 5

loss = (5 - 2)²
     = 9

The gradient tells us which direction to move x. The optimizer then applies that gradient to update x.

Understanding apply_gradients()

This line is very important:

optimizer.apply_gradients([
    (gradient, x)
])

It means:

gradient
    ↓
optimizer
    ↓
update x

In a real neural network, there are many weights:

weight_1
weight_2
weight_3
weight_4
...
weight_n

The optimizer applies the appropriate update to those trainable parameters.

Using Adam Manually

The same idea works with Adam.

import tensorflow as tf

x = tf.Variable(5.0)

optimizer = tf.keras.optimizers.Adam(
    learning_rate=0.1
)

with tf.GradientTape() as tape:

    loss = (x - 2) ** 2

gradient = tape.gradient(loss, x)

optimizer.apply_gradients([
    (gradient, x)
])

print(x.numpy())

The optimizer is different, but the overall process is the same:

Calculate Loss
      ↓
Calculate Gradient
      ↓
Give Gradient to Optimizer
      ↓
Optimizer Updates Variable

Two Practical Examples

Example 1 — Start With Adam

optimizer = tf.keras.optimizers.Adam(
    learning_rate=0.001
)

model.compile(
    optimizer=optimizer,
    loss="binary_crossentropy",
    metrics=["accuracy"]
)

This is a sensible starting point for many beginner projects.

Example 2 — Try SGD

optimizer = tf.keras.optimizers.SGD(
    learning_rate=0.01,
    momentum=0.9
)

model.compile(
    optimizer=optimizer,
    loss="binary_crossentropy",
    metrics=["accuracy"]
)

You can then compare the validation results against the Adam version.

Common Mistake

A common mistake is changing everything at once:

Change:
→ Optimizer
→ Learning rate
→ Batch size
→ Number of layers
→ Number of neurons
→ Activation functions

Then the model improves and you have no idea what caused the improvement.

A better experiment is:

Keep model the same

Change:
Optimizer only

Compare results

Then change another parameter separately.

Remember This

Optimizer
    ↓
Controls weight updates


Adam
    ↓
tf.keras.optimizers.Adam()


SGD
    ↓
tf.keras.optimizers.SGD()


SGD + Momentum
    ↓
tf.keras.optimizers.SGD(
    momentum=0.9
)

The optimizer is normally passed to:

model.compile()

The complete training flow is:

model.fit()
    ↓
Forward Pass
    ↓
Loss
    ↓
Backpropagation
    ↓
Gradients
    ↓
Optimizer
    ↓
Weights Updated
    ↓
Repeat

The key Python pattern to remember is:

optimizer = tf.keras.optimizers.Adam(
    learning_rate=0.001
)

model.compile(
    optimizer=optimizer,
    loss="binary_crossentropy",
    metrics=["accuracy"]
)

model.fit(
    X_train,
    y_train,
    epochs=10
)
QUICK CHECK

Check Your Understanding

Where do we normally specify the optimizer in Keras?
Inside model.compile().

What does the optimizer use to update weights?
Gradients.

What does the learning rate control?
It controls the size of the optimization updates.

How do you create Adam?
tf.keras.optimizers.Adam()

How do you create SGD?
tf.keras.optimizers.SGD()

How do you add momentum to SGD?
Pass a momentum value such as momentum=0.9.

What happens after backpropagation?
The optimizer uses the calculated gradients to update the model's weights.