How To Implement Logistic Regression
Logistic regression is the go-to linear classification algorithm for two-class problems. It is easy to implement, easy to understand and gets great results on a wide variety of problems — even when the assumptions of the method are violated.
In this tutorial you will discover how to implement logistic regression with stochastic gradient descent from scratch with Python — and, right here in your browser, run every snippet interactively.
Description
Logistic regression
Logistic regression is named for the logistic function at the core of the method. It uses a linear combination of the inputs, squashed by the sigmoid, to model the probability of the positive class:
Stochastic gradient descent
Gradient descent is an iterative optimiser: at each step, evaluate the slope of the loss with respect to every parameter and nudge each parameter a small amount in the opposite direction of that slope. Repeat until the loss stops falling. Batch gradient descent averages the gradient over the entire training set before each update; stochastic gradient descent uses the gradient from a single example, so it takes many more, noisier steps per epoch — but each step is cheap and, in practice, the noise helps the optimiser escape flat regions and shallow local minima.
For a single training example the log-loss gradient of logistic regression works out to a neat closed form. Combined with the learning rate η it gives the per-step update rule:
The picture below shows the same idea on a one-dimensional convex loss. Starting from a poor guess, each iteration takes a step proportional to the local slope — long strides where the surface is steep, tiny ones as the parameter approaches the minimum where the gradient vanishes.
Two knobs govern the behaviour. The learning rate η scales every step: too small and training crawls; too large and the updates overshoot the minimum and can diverge. The number of epochs sets how many full passes we make through the training data — each pass gives every example one chance to nudge the coefficients.
1. Making predictions
The first step is a predict() function that takes a row and a set of coefficients and returns a probability. The first coefficient is the intercept (b0).
Hit Run below — the first click loads the Python runtime in your browser (~5 MB), subsequent runs are instant. Edit the code and re-run to experiment.
2. Estimating coefficients
Now we estimate the coefficients with SGD. Three nested loops: over epochs, over rows in the training set, and over coefficients. We track the sum of squared error each epoch so we can watch it fall.
You should see the error drop from ~3.0 to under 0.03 by the last epoch, with the final coefficients printed at the bottom.
How the code works
The whole training loop is just a direct translation of the update rule above. Reading coefficients_sgd from the top:
coef = [0.0 for i in range(len(train[0]))]— one coefficient per input column plus one for the bias, all seeded at zero. With the sigmoid, a zero vector predicts 0.5 for every row, a neutral starting point.for epoch in range(n_epoch)— the outer loop is one full pass over the training data.sum_erroris reset each epoch so we can watch the loss trend downward.yhat = predict(row, coef)thenerror = row[-1] - yhat— compute the current prediction and its signed residual against the true labelrow[-1].coef[0] = coef[0] + l_rate * error * yhat * (1.0 - yhat)— the bias update. Theyhat * (1 - yhat)factor is the derivative of the sigmoid; it damps the update near confident predictions (where the sigmoid saturates) and amplifies it near 0.5 where the model is most uncertain.coef[i + 1] = coef[i + 1] + l_rate * error * yhat * (1.0 - yhat) * row[i]— the weight update for each input, identical to the bias update but scaled by the input valuex_j. Large inputs produce larger corrections, which is exactly why the wine-quality and diabetes examples normalise every column to 0–1 first.
Notice there is no matrix algebra, no autograd, and no library call — the entire optimiser is a dozen lines of arithmetic that faithfully implement the equation shown above.
3. Diabetes prediction
Finally we apply the algorithm to the Pima Indians diabetes dataset — 768 rows, 8 features, one binary label. The runner below fetches the CSV over the network from GitHub, normalises the features, and evaluates the model with 5-fold cross-validation. Training takes ~10–20 seconds in the browser.
Expect a mean accuracy around 77% — well above the 65% Zero-Rule baseline.
Watching the coefficients learn
Rather than only printing a final accuracy, we can log sum_error and the full coef vector at the end of every epoch and plot them. The instrumented loop is a two-line change to coefficients_sgd:
history = []
for epoch in range(n_epoch):
sum_error = 0.0
for row in train:
yhat = predict(row, coef)
error = row[-1] - yhat
sum_error += error ** 2
coef[0] += l_rate * error * yhat * (1.0 - yhat)
for i in range(len(row) - 1):
coef[i + 1] += l_rate * error * yhat * (1.0 - yhat) * row[i]
history.append((epoch, sum_error, list(coef))) # <- record trajectoryRunning that on 600 training rows of the diabetes dataset with η = 0.1 for 100 epochs produces the two curves below. The top chart is the sum-of-squared-error per epoch — a smooth, monotonic descent that flattens as the model approaches its best fit. The bottom chart tracks all nine coefficients as they migrate away from zero.
coefficients_sgd on the Pima Indians diabetes dataset (600 training rows, η = 0.1, 100 epochs). Loss falls monotonically from ≈139 to ≈96 while the nine coefficients drift from zero toward their final values — glucose and BMI end up the strongest positive drivers, blood pressure and skin thickness the weakest.A few things pop out. Glucose and BMI climb fastest and finish with the largest positive weights — high values push the sigmoid toward a "diabetic" prediction, which matches clinical intuition. The bias drifts strongly negative to offset that, keeping the baseline probability low for an average patient. Blood pressure and skin thickness barely move: after normalisation their signal is weak, so the gradient rarely pushes them. This is exactly what "the model is learning" looks like at the level of individual parameters.
Extensions
- Tune the learning rate, epochs, or data preparation.
- Switch stochastic updates for batch updates accumulated across an epoch.
- Apply the technique to other binary classification problems from the UCI ML repository.