Index Ai Academy
Phase 3 · Machine learning · Models

Training with gradient descent

Adjust weights step by step to reduce the loss.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

Linear regression has an exact formula for its best weights. So why train it step by step? Because almost nothing else has one. Logistic regression, neural networks and every LLM learn with the loop you'll write here. Shuffle the data. Take a small batch. Work out which way to nudge the weights. Take a step. Repeat. Once you've written that loop yourself, on a model you can check against the exact answer, every training script you read later will look familiar.

What you'll be able to do

  • Train a regression model on real data with gradient descent, from scratch
  • Use mini-batches, epochs and shuffling the way deep learning does
  • Read a training loss curve
  • Know when gradient descent is needed and when an exact solution exists

What it covers

  1. The training loop
  2. The gradient for one batch
  3. Worked example: one mini-batch step by hand
  4. From scratch on the customer data
  5. Reading loss curves
  6. When do you need gradient descent?

The training loop

Picture a teacher with a pile of 600 practice essays from one student. She could read all 600 before giving any feedback. That's slow. Instead, she reads a handful, gives a quick note, and the student adjusts. Then she reads the next handful. When she reaches the bottom of the pile, she shuffles it and starts again.

Training a model works the same way. The model's weights are the numbers it can adjust. The loss is a score for how wrong its predictions are, and lower is better. In the Phase 2 lesson on walking downhill, you met gradient descent: find the uphill direction for the loss (the gradient), then step the other way. The size of the step is set by the learning rate.

Here is the whole loop in plain steps:

initialize weights (zeros or small random numbers)
repeat for each epoch:
    shuffle the training rows
    for each mini-batch of B rows:
        compute predictions and the gradient on that batch
        weights ← weights − learning_rate × gradient
    record the loss

Three words in it are new:

  • An epoch is one pass over all the training data. It's one trip through the whole pile of essays.
  • A mini-batch is a small random group of rows used for one update. It's the handful the teacher reads before giving a note. Each step is cheap, so training on billions of examples becomes possible.
  • Shuffling mixes the rows before each epoch. Without it, the batches would come in the same order every time. If the data were sorted, say all of one customer group together, each stretch of batches would pull the weights toward that group. Shuffling stops that.

The letter B in the loop is the batch size: how many rows go into each mini-batch.

Try it now

This is the training loop in the lesson, drawn: compute the errors, get the gradient, take a step.

Quick check
You have 10,000 training rows and use mini-batches of 100. How many weight updates happen in one epoch?
  1. 1,000,000
  2. 1
  3. 10,000
  4. 100
Show the answer

100

There's one update per batch. The epoch holds 10,000 / 100 = 100 batches, so it makes 100 updates.

The gradient for one batch

Each update needs the gradient for the batch in hand. For linear regression with mean squared error, you met the formula in Phase 2. Here are its symbols, one at a time:

  • XX is the table of inputs for the batch. It has one row per example and one column per feature.
  • It also has an extra column filled with 1s. The weight on that column acts as the intercept, the starting amount.
  • w\mathbf{w} is the list of weights, one per column.
  • y\mathbf{y} is the list of true values, one per row.
  • nn is the number of rows in the batch.
  • X⊤X^\top, said "X transposed", is the same table flipped so rows become columns.
  • LL is the loss for the batch: the mean squared error.
  • ∇wL\nabla_{\mathbf{w}} L, said "grad L", is the gradient: one number per weight, saying how the loss changes as that weight goes up.
∇wL=2nX⊤(Xw−y)\nabla_{\mathbf{w}} L = \frac{2}{n} X^\top (X\mathbf{w} - \mathbf{y})

In plain words: make a prediction for each row and find its error (prediction minus truth). Then, for each weight, multiply every error by that weight's feature and add them up. Finally, multiply by 2 and divide by the number of rows. A weight whose feature lines up with big errors gets a big gradient, so it moves the most.

The worked example below runs this on two rows, first by hand, then in Python.

Go deeper Where the formula comes from

The loss for a batch is the average of the squared errors. Take one row with error ee. Its squared error is e2e^2, and the slope of e2e^2 is 2e2e. By the chain rule, a weight changes the error at a rate equal to its own feature value. So that row's gradient for the weight is 2 × error × feature. Averaging over the nn rows gives the formula above. The X⊤X^\top part is how matrix math writes "for each weight, add up error times feature".

Worked example

Worked example: one mini-batch step by hand

A batch has two rows with one feature each. The feature values are 1 and 2. The true values are 3 and 5. The weight ww starts at 0. So does the intercept, bb. The learning rate is 0.05.

  • Predictions: with everything at 0, both predictions are 0.
  • Errors: prediction minus truth gives 0 − 3 = −3 and 0 − 5 = −5.
  • Gradient for the weight: error times feature, added up, gives (−3 × 1) + (−5 × 2) = −13. Multiply by 2 and divide by 2 rows: still −13.
  • Gradient for the intercept: its "feature" is always 1, so add up the errors: −3 + −5 = −8. Times 2, divided by 2: still −8.
  • Step: the new weight is 0 − 0.05 × (−13) = 0.65. The new intercept is 0 − 0.05 × (−8) = 0.4.

Run the code to check each line, then see NumPy's one-line version agree.

import numpy as np

x = [1, 2]          # the feature, one value per row in the batch
y = [3, 5]          # the true values
w, b = 0.0, 0.0     # the weight and the intercept, starting at zero
lr = 0.05           # the learning rate
n = len(x)          # rows in this batch

errors = [(w * xi + b) - yi for xi, yi in zip(x, y)]
grad_w = 2 / n * sum(e * xi for e, xi in zip(errors, x))
grad_b = 2 / n * sum(e * 1 for e in errors)
print("errors:", errors)
print("gradient:", grad_w, grad_b)

w = w - lr * grad_w
b = b - lr * grad_b
print("new weight and intercept:", w, b)

X = np.array([[1.0, 1.0], [2.0, 1.0]])   # the feature column, then the column of ones
print("same gradient in NumPy:", 2 / n * X.T @ (X @ np.zeros(2) - np.array(y)))
It runs right here in your browser, with nothing to install.

The errors are −3 and −5, the gradient is −13 and −8, and the step lands on 0.65 and 0.4. Both gradients are negative, which means the loss goes down as these numbers go up. So the step raises both. The last line builds the table XX with its column of 1s and gets the same −13 and −8 from the formula. The loop on the customer data below uses that same NumPy line on batches of 32 rows.

Quick check
A weight is 3.0, its gradient is −4.0, and the learning rate is 0.25. What does this print?
w = 3.0
grad = -4.0
lr = 0.25
w -= lr * grad
print(w)
  1. 2.0
  2. 7.0
  3. 4.0
  4. -1.0
Show the answer

4.0

The update subtracts learning rate times gradient: 3.0 − 0.25 × (−4.0) = 3.0 + 1.0 = 4.0. A negative gradient means downhill is toward bigger values, so the weight goes up.

From scratch on the customer data

Now the full loop on a real task. The shop has 600 customers. The goal is to predict each one's lifetime value (their total spend so far) from four features. Those are orders last year, average order value, months as a customer, and loyalty-club membership.

Before the loop, the code does two set-up steps:

  • It standardizes each feature, as in the data preparation lesson. Each one is rescaled so its average is 0 and its spread is 1.
  • It adds the column of 1s for the intercept.

Then it trains for 30 epochs with batches of 32 and a learning rate of 0.05. The 600 rows make 18 full batches plus one of 24, so 19 updates per epoch and 570 in all. A fixed seed makes the shuffling the same on every run. At the end, it prints the weights next to the exact answer from np.linalg.lstsq ("least squares").

import numpy as np
import pandas as pd
import requests
import matplotlib.pyplot as plt

df = pd.DataFrame(requests.get("https://api.gradient.test/v1/customers").json()["data"])
features = ["orders_last_year", "avg_order_value", "tenure_months", "trail_club"]
X_raw = df[features].to_numpy(dtype=float)
mean, std = X_raw.mean(axis=0), X_raw.std(axis=0)
X = np.column_stack([(X_raw - mean) / std, np.ones(len(df))])    # standardized features + bias column
y = df["lifetime_value"].to_numpy()

rng = np.random.default_rng(0)
w = np.zeros(X.shape[1])
lr, batch, losses = 0.05, 32, []
for epoch in range(30):
    order = rng.permutation(len(y))
    for start in range(0, len(y), batch):
        idx = order[start:start + batch]
        grad = 2 / len(idx) * X[idx].T @ (X[idx] @ w - y[idx])
        w -= lr * grad
    losses.append(np.mean((X @ w - y) ** 2))

exact, *_ = np.linalg.lstsq(X, y, rcond=None)
print("gradient descent:", np.round(w, 1).tolist())
print("exact solution:  ", np.round(exact, 1).tolist())
plt.plot(losses)
plt.yscale("log")
plt.xlabel("epoch")
plt.ylabel("mean squared error")

After 30 epochs, every weight is within about $10 of the exact answer. It doesn't land exactly on it. Each batch gives a slightly different gradient, so the weights keep jiggling around the best spot. A learning rate that shrinks late in training closes that gap. That's the next lesson.

The chart is the loss curve, one point per epoch. The loss drops in the first epoch or two, then flattens into a slightly jagged line. That's the classic shape.

Go deeper Why standardize first?

One learning rate is shared by every weight. In this data, orders last year run from 0 to 18, while average order value runs from about $18 to $194. Left unscaled, the same step size would be too big for one weight and too small for the other. The loss surface becomes a long, narrow valley, and gradient descent zig-zags down it slowly. Putting every feature on the same scale lets one learning rate suit them all. Forgetting this step often makes training crawl or blow up.

Go deeper Why the mini-batch steps are noisy

The gradient from 32 rows is an estimate of the gradient from all 600. It's like tasting a spoonful to judge the whole pot of soup. Most spoonfuls taste about right, but each one is a little different. So each step points roughly downhill, never perfectly. On average the steps still lead to the bottom, and each one costs a fraction of a full pass.

Reading loss curves

The loss curve is the first thing to check after any training run. Here's what its common shapes usually mean:

ShapeUsual meaning
drops then flattensconverged; more epochs won't help
still falling at the endtrain longer (or raise the learning rate)
jagged but trending downnormal with small batches; the noise is the batch sampling
rising or explodinglearning rate too high, or a bug
training loss falls, validation loss risesoverfitting (a later lesson)

"Converged" means the weights have settled near the best spot. "Validation" data is data held back from training, as in the lesson on train, validation and test sets.

Always plot the loss on held-out data too. A model is only getting better if that curve falls. A falling training loss can also mean the model is memorizing its training rows.

Quick check
After a few epochs your loss starts climbing, then shoots up to huge numbers. What's the most likely cause?
  1. The learning rate is too high, or there's a bug
  2. This is normal noise from small batches
  3. The batches are shuffled
  4. The model has converged
Show the answer

The learning rate is too high, or there's a bug

Small-batch noise makes the curve jagged, but it still trends down. A loss that keeps rising or explodes usually means the steps overshoot the bottom, or the code has a bug.

When do you need gradient descent?

ModelExact solution?How it's trained
Linear regressionyes (least squares)exact for small data; gradient descent for huge data
Logistic regressionnoiterative optimization (gradient-based)
Decision trees, forestsno gradients neededgreedy splitting (later lessons)
Neural networks, LLMsnomini-batch gradient descent (usually Adam)

For linear regression on 600 customers, you'd use the exact solution in practice. What you keep from this lesson is the loop itself. A PyTorch training script, which you'll write in Phase 4, has the same shape. PyTorch is a popular deep-learning library. Look for the familiar parts: epochs, shuffled batches, a loss, the gradient and the update.

for epoch in range(epochs):
    for xb, yb in loader:                 # shuffled mini-batches
        loss = ((model(xb) - yb) ** 2).mean()
        optimizer.zero_grad()
        loss.backward()                   # the gradient, by backpropagation
        optimizer.step()                  # w ← w − lr × gradient (or Adam's version)

The loader hands out shuffled mini-batches. The line loss.backward() works out the gradient for you, using a method called backpropagation. The line optimizer.step() applies the update. Adam, mentioned in the table, is a smarter version of that update and the usual choice for neural networks.

Quick check
You're fitting linear regression to 600 customers on a laptop. What's the sensible way to find the weights?
  1. Greedy splitting, as in decision trees
  2. Mini-batch gradient descent for thousands of epochs
  3. Use the exact least-squares solution
  4. There's no way to find them without a neural network
Show the answer

Use the exact least-squares solution

Linear regression has an exact solution, and 600 rows is tiny. Gradient descent is for models with no exact formula, or data too large to solve in one go.

Practice what you just read

  • 3 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 4 review cards for spaced repetition

Create a free account

Try a quiz question

What is an epoch?

  1. One full pass over the training data
  2. One weight update, made after a single batch
  3. One mini-batch of rows, drawn at random
  4. One prediction, made for a single training row

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson