Skip to content

Exercises: Intro to Deep Learning

These exercises move from concept confirmation to implementation to diagnosis. Work through them in order. Each builds on the previous one. By the end you will have a complete training, evaluation, and regularization workflow you can adapt to any classification problem.


Warm-Up Exercises

These confirm your understanding of the concepts before you write model code.

Warm-Up 1: Trace a Forward Pass by Hand

Given the following single neuron:

  • Inputs: x = [2.0, -1.0, 0.5]
  • Weights: w = [0.3, -0.5, 0.8]
  • Bias: b = 0.1
  • Activation: ReLU

Calculate the output without using a neural network library.

import numpy as np

x = np.array([2.0, -1.0, 0.5])
w = np.array([0.3, -0.5, 0.8])
b = 0.1

# Your code here
z = # ?
output = # ?

print(f"z = {z}")
print(f"output = {output}")
Show answer
import numpy as np

x = np.array([2.0, -1.0, 0.5])
w = np.array([0.3, -0.5, 0.8])
b = 0.1

z = np.dot(w, x) + b
# z = (0.3)(2.0) + (-0.5)(-1.0) + (0.8)(0.5) + 0.1
# z = 0.6 + 0.5 + 0.4 + 0.1 = 1.6
output = np.maximum(0, z)

print(f"z = {z}")        # z = 1.6
print(f"output = {output}")  # output = 1.6

Warm-Up 2: Match the Output Activation

For each task below, write the correct output layer in Keras:

  1. Predict whether a loan will default (binary)
  2. Classify images into 10 categories
  3. Predict tomorrow's temperature in Celsius
  4. Predict whether each of 5 disease symptoms is present (multi-label)
Show answer
from tensorflow import keras

# 1. Binary classification
output_binary = keras.layers.Dense(1, activation="sigmoid")

# 2. 10-class classification
output_multiclass = keras.layers.Dense(10, activation="softmax")

# 3. Regression
output_regression = keras.layers.Dense(1)  # no activation

# 4. Multi-label (each label is independent)
output_multilabel = keras.layers.Dense(5, activation="sigmoid")

Warm-Up 3: Identify the Problem

Look at these training curves (described below) and name what is wrong and why:

  • Curve A: Train loss: 0.68 → 0.55 → 0.49 → 0.44. Val loss: 0.70 → 0.62 → 0.58 → 0.56. Both still decreasing at epoch 20.
  • Curve B: Train loss: 0.68 → 0.30 → 0.12 → 0.04. Val loss: 0.70 → 0.55 → 0.65 → 0.82.
  • Curve C: Train loss: NaN from epoch 1.
Show answer
  • Curve A: The model is underfitting. Both curves are still declining at the end of training, meaning the model has not yet converged. Fix: train for more epochs, or use a larger/more expressive model.

  • Curve B: Classic overfitting. Train loss falls aggressively while val loss starts rising after epoch 2. The model is memorizing the training data. Fix: add Dropout, reduce model size, or use EarlyStopping.

  • Curve C: Loss is NaN immediately. This indicates numerical instability — almost always caused by a learning rate that is too high, inputs that are not scaled (StandardScaler was not applied), or a mismatched output activation and loss function (e.g., linear output with cross-entropy loss). Fix: check scaling, check that output activation matches the loss, reduce learning rate by 10x.


Main Exercises

Exercise 1: Build and Train a Binary Classifier

Build a neural network on the synthetic dataset below. Your model should achieve at least 85% validation accuracy.

Requirements: - At least 2 hidden layers with ReLU activation - Binary cross-entropy loss - Adam optimizer - Plot training and validation loss curves - Report test accuracy

import numpy as np
import tensorflow as tf
from tensorflow import keras
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# Dataset
X, y = make_classification(
    n_samples=4000,
    n_features=20,
    n_informative=10,
    n_redundant=5,
    random_state=7,
)

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=7
)

# Scale your data
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test  = scaler.transform(X_test)

# Your code: build model, compile, train, evaluate, plot
# ...
Show answer
import numpy as np
import tensorflow as tf
from tensorflow import keras
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

X, y = make_classification(
    n_samples=4000, n_features=20, n_informative=10,
    n_redundant=5, random_state=7,
)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=7
)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test  = scaler.transform(X_test)

model = keras.Sequential([
    keras.layers.Dense(64, activation="relu", input_shape=(20,)),
    keras.layers.Dense(32, activation="relu"),
    keras.layers.Dense(1,  activation="sigmoid"),
])

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

history = model.fit(
    X_train, y_train,
    validation_split=0.2,
    epochs=50,
    batch_size=32,
    verbose=0,
)

test_loss, test_acc = model.evaluate(X_test, y_test, verbose=0)
print(f"Test accuracy: {test_acc:.4f}")
# Output: Test accuracy: ~0.88

plt.figure(figsize=(10, 4))
plt.plot(history.history["loss"],     label="Train loss")
plt.plot(history.history["val_loss"], label="Val loss", linestyle="--")
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.title("Training Curves")
plt.legend()
plt.show()

Exercise 2: Overfit, Then Fix It

Step 1: Deliberately overfit the dataset from Exercise 1 by building a much larger model (e.g., three layers of 512 neurons each) and training for 100 epochs. Confirm that overfitting occurs by looking at the training curves.

Step 2: Apply two regularization techniques to reduce the gap between training and validation loss.

# Step 1: Build an overfit model
overfit_model = keras.Sequential([
    # Your large model here
])

# Train and plot — confirm overfitting

# Step 2: Build a regularized version
regularized_model = keras.Sequential([
    # Same or smaller architecture with Dropout and/or L2
])

# Train and compare curves side-by-side
Show answer
from tensorflow.keras import regularizers

# Step 1: Overfit model
overfit_model = keras.Sequential([
    keras.layers.Dense(512, activation="relu", input_shape=(20,)),
    keras.layers.Dense(512, activation="relu"),
    keras.layers.Dense(512, activation="relu"),
    keras.layers.Dense(1,   activation="sigmoid"),
])
overfit_model.compile(optimizer="adam", loss="binary_crossentropy",
                      metrics=["accuracy"])
h_overfit = overfit_model.fit(
    X_train, y_train, validation_split=0.2,
    epochs=100, batch_size=32, verbose=0
)

# Step 2: Regularized model
reg_model = keras.Sequential([
    keras.layers.Dense(128, activation="relu", input_shape=(20,),
                       kernel_regularizer=regularizers.l2(0.001)),
    keras.layers.Dropout(0.4),
    keras.layers.Dense(64, activation="relu",
                       kernel_regularizer=regularizers.l2(0.001)),
    keras.layers.Dropout(0.4),
    keras.layers.Dense(1, activation="sigmoid"),
])
reg_model.compile(optimizer="adam", loss="binary_crossentropy",
                  metrics=["accuracy"])

early_stop = keras.callbacks.EarlyStopping(
    monitor="val_loss", patience=10, restore_best_weights=True
)
h_reg = reg_model.fit(
    X_train, y_train, validation_split=0.2,
    epochs=100, batch_size=32, callbacks=[early_stop], verbose=0
)

# Side-by-side comparison
fig, axes = plt.subplots(1, 2, figsize=(14, 4))
for ax, h, title in [(axes[0], h_overfit, "No Regularization"),
                      (axes[1], h_reg,    "With Dropout + L2")]:
    ax.plot(h.history["loss"],     label="Train")
    ax.plot(h.history["val_loss"], label="Val", linestyle="--")
    ax.set_title(title)
    ax.set_xlabel("Epoch")
    ax.set_ylabel("Loss")
    ax.legend()
plt.tight_layout()
plt.show()

Exercise 3: Add EarlyStopping and Plot the Stopping Point

Train a model with EarlyStopping(patience=8). After training, plot the loss curves and add a vertical line at the epoch where training stopped.

early_stop = keras.callbacks.EarlyStopping(
    monitor="val_loss",
    patience=8,
    restore_best_weights=True,
    verbose=1,
)

# Your model and training here

# After training:
# stopped_epoch = ???
# best_epoch    = ???

# Plot with vertical lines at stopped epoch and best epoch
Show answer
model = keras.Sequential([
    keras.layers.Dense(128, activation="relu", input_shape=(20,)),
    keras.layers.Dropout(0.3),
    keras.layers.Dense(64,  activation="relu"),
    keras.layers.Dense(1,   activation="sigmoid"),
])
model.compile(optimizer="adam", loss="binary_crossentropy",
              metrics=["accuracy"])

early_stop = keras.callbacks.EarlyStopping(
    monitor="val_loss", patience=8,
    restore_best_weights=True, verbose=1
)

history = model.fit(
    X_train, y_train,
    validation_split=0.2,
    epochs=200,
    batch_size=32,
    callbacks=[early_stop],
    verbose=0,
)

stopped_epoch = early_stop.stopped_epoch
# Best epoch is stopped_epoch - patience (approximately)
best_epoch = stopped_epoch - early_stop.patience

plt.figure(figsize=(10, 4))
plt.plot(history.history["loss"],     label="Train loss")
plt.plot(history.history["val_loss"], label="Val loss", linestyle="--")
plt.axvline(stopped_epoch, color="red",    linestyle=":", label=f"Stopped: epoch {stopped_epoch}")
plt.axvline(best_epoch,    color="green",  linestyle=":", label=f"Best:    epoch {best_epoch}")
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.title("Training with EarlyStopping")
plt.legend()
plt.show()

Exercise 4: Compare Neural Network vs. Gradient Boosting

Use the same dataset from Exercise 1. Train both a neural network and an XGBoost (or Random Forest) classifier. Report accuracy, training time, and inference time for both. Write a one-sentence conclusion about which you would use in production.

import time
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import accuracy_score

# Neural network (from Exercise 1)
# ... your model

# Gradient boosting
gb = GradientBoostingClassifier(n_estimators=200, random_state=42)

# Time training for both
# Report: accuracy, training time, prediction time
# Conclusion: which would you deploy and why?
Show answer
import time
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import accuracy_score

# Neural network
nn_model = keras.Sequential([
    keras.layers.Dense(64, activation="relu", input_shape=(20,)),
    keras.layers.Dense(32, activation="relu"),
    keras.layers.Dense(1,  activation="sigmoid"),
])
nn_model.compile(optimizer="adam", loss="binary_crossentropy",
                 metrics=["accuracy"])

t0 = time.time()
nn_model.fit(X_train, y_train, epochs=30, batch_size=32, verbose=0)
nn_train_time = time.time() - t0

t0 = time.time()
nn_proba = nn_model.predict(X_test, verbose=0)
nn_pred_time = time.time() - t0
nn_preds = (nn_proba >= 0.5).astype(int).flatten()
nn_acc = accuracy_score(y_test, nn_preds)

# Gradient Boosting (does not need scaling)
X_train_raw, X_test_raw, _, _ = train_test_split(
    X, y, test_size=0.2, random_state=7
)

gb = GradientBoostingClassifier(n_estimators=200, random_state=42)
t0 = time.time()
gb.fit(X_train_raw, y_train)
gb_train_time = time.time() - t0

t0 = time.time()
gb_preds = gb.predict(X_test_raw)
gb_pred_time = time.time() - t0
gb_acc = accuracy_score(y_test, gb_preds)

print(f"{'Model':<25} {'Accuracy':>10} {'Train time':>12} {'Predict time':>14}")
print("-" * 65)
print(f"{'Neural Network':<25} {nn_acc:>10.4f} {nn_train_time:>11.2f}s {nn_pred_time:>13.4f}s")
print(f"{'Gradient Boosting':<25} {gb_acc:>10.4f} {gb_train_time:>11.2f}s {gb_pred_time:>13.4f}s")

# Expected output (approximate):
# Model                      Accuracy   Train time   Predict time
# Neural Network               0.8800        8.50s         0.0500s
# Gradient Boosting            0.8925        1.20s         0.0020s

print("\nConclusion: On this tabular dataset with ~4,000 samples, "
      "Gradient Boosting matches or beats the neural network in accuracy "
      "while training 5-10x faster and requiring no feature scaling. "
      "For production, Gradient Boosting is the better choice here.")

Stretch Exercises

Stretch 1: Multi-class Classification with Confusion Matrix

Using make_classification with 5 classes, build a classifier and: 1. Plot the training curves 2. Generate a confusion matrix on the test set 3. Report per-class precision, recall, and F1 using classification_report 4. Identify which class the model struggles with most and hypothesize why

Show answer
from sklearn.datasets import make_classification
from sklearn.metrics import confusion_matrix, classification_report, ConfusionMatrixDisplay

X5, y5 = make_classification(
    n_samples=5000, n_features=20, n_classes=5,
    n_informative=10, n_redundant=5, random_state=42
)
X5_train, X5_test, y5_train, y5_test = train_test_split(
    X5, y5, test_size=0.2, random_state=42
)
scaler5 = StandardScaler()
X5_train = scaler5.fit_transform(X5_train)
X5_test  = scaler5.transform(X5_test)

model5 = keras.Sequential([
    keras.layers.Dense(128, activation="relu", input_shape=(20,)),
    keras.layers.Dropout(0.3),
    keras.layers.Dense(64,  activation="relu"),
    keras.layers.Dense(5,   activation="softmax"),  # 5 classes
])
model5.compile(optimizer="adam",
               loss="sparse_categorical_crossentropy",
               metrics=["accuracy"])

history5 = model5.fit(
    X5_train, y5_train,
    validation_split=0.2, epochs=50, batch_size=64, verbose=0
)

y5_pred_proba = model5.predict(X5_test, verbose=0)
y5_pred = np.argmax(y5_pred_proba, axis=1)

print(classification_report(y5_test, y5_pred,
                             target_names=[f"Class {i}" for i in range(5)]))

cm = confusion_matrix(y5_test, y5_pred)
disp = ConfusionMatrixDisplay(confusion_matrix=cm,
                               display_labels=[f"C{i}" for i in range(5)])
disp.plot(cmap="Blues")
plt.title("Confusion Matrix — 5-class classifier")
plt.show()

Stretch 2: Learning Rate Finder

Implement a learning rate range test: train the model for a single epoch while logging the loss at each step and increasing the learning rate exponentially from 1e-6 to 1. Plot loss vs. learning rate. The optimal starting learning rate is just before the loss starts increasing steeply.

Show answer
import tensorflow as tf
from tensorflow import keras
import numpy as np
import matplotlib.pyplot as plt

def lr_finder(model, X_train, y_train,
              start_lr=1e-6, end_lr=1.0, num_steps=100, batch_size=32):
    """Run a learning rate range test."""
    lrs = np.geomspace(start_lr, end_lr, num_steps)
    losses = []

    # Save initial weights to reset after the test
    initial_weights = model.get_weights()

    optimizer = keras.optimizers.Adam()
    loss_fn   = keras.losses.BinaryCrossentropy()

    dataset = tf.data.Dataset.from_tensor_slices((X_train, y_train))
    dataset = dataset.batch(batch_size).take(num_steps)

    for step, (x_batch, y_batch) in enumerate(dataset):
        tf.keras.backend.set_value(optimizer.learning_rate, lrs[step])

        with tf.GradientTape() as tape:
            preds = model(x_batch, training=True)
            loss  = loss_fn(y_batch, preds)

        grads = tape.gradient(loss, model.trainable_variables)
        optimizer.apply_gradients(zip(grads, model.trainable_variables))
        losses.append(float(loss))

        if float(loss) > 4 * losses[0]:  # stop if loss explodes
            break

    # Restore weights
    model.set_weights(initial_weights)

    # Plot
    plt.figure(figsize=(8, 4))
    plt.semilogx(lrs[:len(losses)], losses)
    plt.xlabel("Learning Rate (log scale)")
    plt.ylabel("Loss")
    plt.title("Learning Rate Finder")
    plt.show()

    # Suggested lr: where loss starts its steepest decline
    smoothed = np.convolve(losses, np.ones(5)/5, mode="valid")
    best_idx = np.argmin(np.gradient(smoothed))
    print(f"Suggested learning rate: {lrs[best_idx]:.2e}")

test_model = keras.Sequential([
    keras.layers.Dense(64, activation="relu", input_shape=(20,)),
    keras.layers.Dense(32, activation="relu"),
    keras.layers.Dense(1,  activation="sigmoid"),
])

lr_finder(test_model, X_train, y_train)

Stretch 3: Manually Implement One Training Step

Without calling model.fit(), implement a single training step using tf.GradientTape. Train for 5 epochs with a manual loop and confirm the loss decreases.

Show answer
import tensorflow as tf
from tensorflow import keras
import numpy as np

# Data (using scaled X_train, y_train from Exercise 1)
dataset = tf.data.Dataset.from_tensor_slices(
    (X_train.astype(np.float32), y_train.astype(np.float32))
).shuffle(1000).batch(32)

model = keras.Sequential([
    keras.layers.Dense(64, activation="relu", input_shape=(20,)),
    keras.layers.Dense(32, activation="relu"),
    keras.layers.Dense(1,  activation="sigmoid"),
])

optimizer = keras.optimizers.Adam(learning_rate=0.001)
loss_fn   = keras.losses.BinaryCrossentropy()

@tf.function  # compile the step for speed
def train_step(x_batch, y_batch):
    with tf.GradientTape() as tape:
        predictions = model(x_batch, training=True)
        loss = loss_fn(y_batch, predictions)

    gradients = tape.gradient(loss, model.trainable_variables)
    optimizer.apply_gradients(zip(gradients, model.trainable_variables))
    return loss

for epoch in range(5):
    epoch_loss = 0.0
    steps = 0
    for x_batch, y_batch in dataset:
        batch_loss = train_step(x_batch, y_batch)
        epoch_loss += float(batch_loss)
        steps += 1

    print(f"Epoch {epoch+1}/5 — Loss: {epoch_loss / steps:.4f}")

# Output:
# Epoch 1/5 — Loss: 0.5821
# Epoch 2/5 — Loss: 0.4413
# Epoch 3/5 — Loss: 0.3987
# Epoch 4/5 — Loss: 0.3741
# Epoch 5/5 — Loss: 0.3592

Interview Questions

Q1: Explain backpropagation to someone who has not heard of it.

Show answer

Backpropagation is the algorithm that figures out how much each weight in a neural network contributed to the prediction error, so the optimizer knows how to adjust them. It works by flowing the error signal backward through the network from output to input, using the chain rule of calculus at each layer. Think of it as blame propagation: the network made a wrong prediction, and backprop assigns a share of the blame to every weight, proportional to how much changing that weight would change the final error.

Q2: Your model's loss is NaN after epoch 1. What do you check first?

Show answer

In order: (1) check that StandardScaler was applied — large unscaled inputs cause gradient explosion; (2) check that the output activation matches the loss function — using sigmoid output with MSE loss, for example, will cause numerical issues; (3) cut the learning rate by 10x — even with scaled inputs, too high a learning rate causes gradient explosion; (4) check for NaN or infinite values in the input data using np.isnan(X).sum() and np.isinf(X).sum().

Q3: When would you use BatchNormalization instead of Dropout?

Show answer

BatchNormalization is preferred when training is unstable — loss curves are noisy or the model is sensitive to initialization. It normalizes activations within each batch, which stabilizes the gradient signal and often allows larger learning rates. Dropout is preferred when the model clearly overfits (training loss falls while val loss rises). For very deep networks, BatchNorm is almost always included. For smaller fully-connected networks on tabular data, Dropout is usually sufficient. Avoid using both on the same layer — their interactions are poorly understood and often counterproductive.

Q4: You have 50,000 rows of tabular data with 30 features. Should you use a neural network or gradient boosting?

Show answer

Start with gradient boosting (XGBoost or LightGBM). At 50k rows and 30 features, it will train in seconds, require no feature scaling, handle missing values natively, and be interpretable via SHAP values. A neural network might match its performance after careful hyperparameter tuning, but the expected gain is marginal on structured tabular data of this size. Switch to a neural network only if (1) gradient boosting is already well-tuned and you need further improvement, (2) the features are embeddings or high-dimensional sparse vectors, or (3) you need to learn representations jointly with another task.


Back: Overfitting and Regularization | Next: NLP Basics