- Derive the least-squares normal equation and implement linear regression with both a solver and gradient descent.
- Interpret regression coefficients, odds ratios and R-squared, and state the assumptions behind them.
- Explain the sigmoid, log-odds, decision boundary and log-loss of logistic regression and compute them by hand.
- Compare ridge, lasso and elastic net regularisation and choose among them for a given dataset.
- Extend binary logistic regression to multiclass problems using softmax or one-vs-rest.
Before this: The ML Workflow: Framing, Data, Splits and Leakage, Linear Algebra for AI, Calculus and Gradients: How Networks Learn
Linear and logistic regression are the workhorses of applied machine learning. They are the first models you should fit on almost any tabular problem, they are still used directly in credit scoring, medicine, advertising and economics because their coefficients can be read and audited, and their machinery (a weighted sum, a loss function, gradient descent, regularisation, softmax) is exactly what sits inside every neural network. If you understand this lesson, the output layer of a transformer holds no mysteries.
Intuition: the best straight edge
Imagine a scatter of points on graph paper and a ruler. Linear regression slides and tilts the ruler until the total squared vertical distance from the points to the ruler is as small as possible. Squaring makes large misses count much more than small ones, and it gives a smooth bowl-shaped error surface with one lowest point that we can find exactly.
Logistic regression uses the same ruler for a yes/no question. Instead of predicting the value directly, it computes a score along the ruler and squashes that score into a probability between 0 and 1. The ruler's line (where the probability is exactly 0.5) becomes a decision boundary separating the two classes.
Linear regression
The model
We have examples. Example has a feature vector and a real-valued target . The model predicts
with weights (coefficients) and intercept (bias) . It is convenient to append a constant 1 to every and fold into the weight vector; stacking all examples as rows gives a design matrix and the predictions , where .
Least squares
We choose to minimise the mean squared error (MSE):
Squared error is not arbitrary: if the targets are generated as with independent Gaussian noise , minimising MSE is the same as maximising the likelihood of the data.
The normal equation
is a convex quadratic in , so its minimum is where the gradient is zero:
These are the normal equations. If is invertible (no feature is an exact linear combination of others),
In code, never form the inverse explicitly; call a least-squares solver (np.linalg.lstsq), which uses numerically stable QR or SVD decompositions. Solving costs roughly , which is fine for thousands of features but not millions.
Gradient descent
For very large or , or when no closed form exists (as in logistic regression), we use gradient descent: start from some and repeatedly step downhill,
where the learning rate sets the step size. Too large and the iterates diverge; too small and progress crawls. Stochastic gradient descent (SGD) estimates the gradient from a small random batch of examples, trading noise for much cheaper steps. Gradient descent converges much faster when features are on similar scales, which is one reason to standardise them.
Worked example: three points
Data: . With one feature, the least-squares solution has the familiar form
- Means: , .
- Numerator: .
- Denominator: .
- So and .
Predictions are $1.833, 3.333, 4.8330.167, -0.333, 0.167$.
One step of gradient descent from with , for comparison: the residuals are , so and . The update gives , : already close to the answer, and further steps converge to .
Measuring fit:
The coefficient of determination compares the model's squared error with that of always predicting the mean:
In the example, and , so : the line explains about 96% of the variance in . On training data never decreases when you add features, so judge it on held-out data, where it can even be negative (worse than predicting the mean).
Assumptions, and which ones matter
Linear regression fits any data. The assumptions matter for whether the fit is good and whether standard errors and p-values are trustworthy:
- Linearity: the expected target is linear in the features (after any transformations you apply). Check residual-vs-fitted plots for curves.
- Independent errors: violated by time series and repeated measurements; standard errors become too small.
- Constant error variance (homoscedasticity): violated when errors grow with the prediction, common for prices and counts; a log transform of often helps.
- Normal errors: needed only for exact small-sample inference; irrelevant for prediction.
- No perfect multicollinearity: if features are exact linear combinations, is singular. Strong (not perfect) correlation makes individual coefficients unstable even when predictions are fine.
For pure prediction, only (1) truly matters, and it is checked by validation error. For explanation, all of them do.
Interpreting coefficients
is the expected change in for a one-unit increase in feature , holding the other features fixed. Three cautions:
- Units matter. A coefficient of 0.001 on income in dollars is not "small". Standardise features (subtract the mean, divide by the standard deviation) to compare magnitudes; then is the change per standard deviation.
- "Holding others fixed" may be impossible in reality when features are correlated (you cannot raise square footage while holding room count fixed), and correlated features can split or flip each other's coefficients.
- Coefficients describe association, not causation. A positive coefficient on "number of firefighters" for fire damage does not mean firefighters cause damage.
Logistic regression
From scores to probabilities
For binary labels we want . A linear score can be any real number, so we pass it through the sigmoid (logistic) function:
maps to , with and .
Odds and log-odds
The odds of an event with probability are ; a probability of 0.8 is odds of 4 (to 1). Inverting the sigmoid shows what logistic regression really models:
The log-odds (logit) are linear in the features. So a one-unit increase in adds to the log-odds, which multiplies the odds by (the odds ratio). With , each unit multiplies the odds by . Note that this is not a fixed change in probability: the same odds ratio moves from 0.5 to 0.67, but from 0.95 to only 0.97.
The decision boundary
Predicting class 1 when is the same as predicting it when . The boundary is a straight line (a hyperplane in dimensions), so logistic regression is a linear classifier. Curved boundaries require transformed features (squares, interactions, splines). The 0.5 threshold is a choice: when false negatives cost more than false positives, lower it.
Log-loss
The squared error would make the optimisation non-convex here. Instead we maximise the likelihood , which is equivalent to minimising the log-loss (binary cross-entropy):
It punishes confident mistakes severely: predicting for a true positive costs , while costs only . is convex, and its gradient has a strikingly simple form,
"prediction minus target, times input", the same shape as for linear regression. There is no closed-form solution, so we use iterative solvers (gradient descent, Newton's method, L-BFGS). If the classes are perfectly separable, the unregularised weights grow without bound; regularisation fixes this.
Worked example
A model has , for a single feature. For : , so .
- If the true label is , the log-loss for this example is .
- If , it is .
- The odds are . Increasing by 1 multiplies the odds by , to , i.e. (check: ).
- The decision boundary is at , i.e. .
Regularisation: ridge, lasso, elastic net
With many features, few examples or correlated features, least-squares and maximum-likelihood weights become large and unstable, and the model overfits. Regularisation adds a penalty on the size of the weights. For a data loss (MSE or log-loss) and strength :
The intercept is not penalised. (Exact scaling constants differ between textbooks and libraries; scikit-learn's Ridge minimises without dividing by , while its Lasso divides the squared error by .)
- Ridge shrinks all weights smoothly toward zero and has a closed form, on centred data. Adding makes the matrix invertible even with collinear features. Among correlated features, ridge tends to share weight between them.
- Lasso drives some weights exactly to zero, performing feature selection. Geometrically, the L1 constraint region is a diamond whose corners lie on the axes, and the optimum often lands on a corner. Among a group of highly correlated features, lasso tends to pick one somewhat arbitrarily.
- Elastic net mixes both: sparse like lasso, but stable with correlated groups like ridge.
Regularisation penalises all weights equally, so features must be standardised first, or the penalty will hit small-unit features harder. Choose by cross-validation. In scikit-learn, linear regressors use alpha for , but LogisticRegression uses C, the inverse strength: small C means strong regularisation. scikit-learn's LogisticRegression applies L2 regularisation with C=1.0 by default, which surprises people expecting a plain maximum-likelihood fit.
Multiclass classification
For classes there are two standard approaches.
One-vs-rest (OvR) trains binary classifiers, class against all others, and predicts the class with the highest score. Simple and parallel, but the probabilities are not jointly normalised.
Multinomial (softmax) regression gives each class its own weight vector and bias , computes scores , and normalises them with the softmax:
Training minimises the cross-entropy , which reduces to the log-loss when . For example, scores give , summing to 11.21, so the probabilities are ; if the true class is the first, the loss is . This exact computation is the final layer of nearly every neural network classifier, including the next-token prediction of language models. Current versions of scikit-learn's LogisticRegression use the multinomial formulation for multiclass problems with its default solver; wrap a model in OneVsRestClassifier if you specifically want OvR.
Code
First, three ways to fit the same linear regression, which should agree:
import numpy as np
from sklearn.linear_model import LinearRegression
rng = np.random.default_rng(0)
n = 200
x = rng.uniform(0, 10, size=(n, 1))
y = 3.0 + 2.0 * x[:, 0] + rng.normal(0, 1.0, size=n) # true b=3, w=2
# Normal equation (use lstsq/solve, never an explicit inverse)
X = np.hstack([np.ones((n, 1)), x]) # add bias column
theta_ne = np.linalg.lstsq(X, y, rcond=None)[0]
# Batch gradient descent on MSE
theta = np.zeros(2)
lr = 0.01
for _ in range(5000):
grad = (2 / n) * X.T @ (X @ theta - y)
theta -= lr * grad
sk = LinearRegression().fit(x, y)
print("normal eq :", theta_ne.round(3))
print("grad desc :", theta.round(3))
print("sklearn :", round(sk.intercept_, 3), sk.coef_.round(3))
print("R^2 :", round(sk.score(x, y), 3))
All three recover , (close to the true 3 and 2; the difference is noise), with .
Next, regularised regression on scikit-learn's diabetes dataset (442 patients, 10 features, target is a disease progression score):
import numpy as np
from sklearn.datasets import load_diabetes
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression, Ridge, Lasso, ElasticNet
X, y = load_diabetes(return_X_y=True)
models = {
"OLS": LinearRegression(),
"Ridge a=10": Ridge(alpha=10),
"Lasso a=1": Lasso(alpha=1.0),
"ENet a=1": ElasticNet(alpha=1.0, l1_ratio=0.5),
}
for name, m in models.items():
pipe = make_pipeline(StandardScaler(), m)
r2 = cross_val_score(pipe, X, y, cv=5, scoring="r2").mean()
coef = pipe.fit(X, y)[-1].coef_
print(f"{name:11s} CV R2={r2:.3f} zero coefs={np.sum(coef == 0)}/10")
Typical output: all of OLS, ridge and lasso reach a cross-validated of about 0.48, but lasso does it with 3 of the 10 coefficients set exactly to zero, a simpler model at no cost. The elastic net at this alpha is over-regularised (): the strength must be tuned (for example with LassoCV, RidgeCV or ElasticNetCV), not guessed.
Finally, logistic regression with interpretable coefficients:
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import log_loss, accuracy_score
data = load_breast_cancer()
X_tr, X_te, y_tr, y_te = train_test_split(
data.data, data.target, test_size=0.25, stratify=data.target, random_state=0)
clf = make_pipeline(StandardScaler(), LogisticRegression(C=1.0, max_iter=1000))
clf.fit(X_tr, y_tr)
p = clf.predict_proba(X_te)[:, 1]
print("test log-loss: %.3f accuracy: %.3f" % (log_loss(y_te, p), accuracy_score(y_te, p > 0.5)))
coef = clf[-1].coef_[0]
top = np.argsort(np.abs(coef))[::-1][:3]
for i in top: # odds ratio per +1 standard deviation of the feature
print(f"{data.feature_names[i]:22s} w={coef[i]:+.2f} odds x{np.exp(coef[i]):.2f}")
This reaches about 0.96 test accuracy with a log-loss near 0.09. Because features were standardised, each coefficient is per standard deviation. In this dataset class 1 is benign, so the negative weights (for example on "radius error", about , odds ratio about 0.30) mean larger values make benign less likely: each extra standard deviation cuts the odds of benign to roughly a third. Remember the caveat from earlier: many of these 30 features are strongly correlated (radius, perimeter and area measure nearly the same thing), so individual coefficients should be interpreted cautiously.
Common pitfalls
- Not scaling before regularising, so the penalty depends on arbitrary units.
- Forgetting that
LogisticRegressionis regularised by default and thatCis an inverse strength. - Reading coefficients causally, or comparing raw coefficients across features with different units.
- Interpreting coefficients of strongly correlated features individually; they can split, shrink or flip sign.
- Reporting training as evidence of predictive power.
- Using linear regression for a binary target, which yields "probabilities" below 0 or above 1.
- Treating 0.5 as the only threshold; choose the threshold from the costs of errors.
- Computing the inverse explicitly instead of using a stable solver.
In practice
- Fit a regularised linear or logistic model as your first serious baseline on every tabular problem. It trains in seconds, rarely fails badly and tells you whether the signal is roughly linear.
- Standardise numeric features and one-hot encode categoricals inside a pipeline; tune
alphaorCon a logarithmic grid by cross-validation. - Use L1 or elastic net when you want a sparse, explainable model; ridge when you want stability with many correlated features.
- For heavily skewed targets (prices, durations), model ; for counts, consider Poisson regression (
PoissonRegressor). - Logistic regression's probabilities are usually reasonably calibrated, which is why it remains standard in credit scoring and clinical risk scores; check calibration anyway (covered in
model-evaluation-and-metrics). - When the linear baseline is clearly beaten by tree ensembles (next lesson), that tells you the signal involves interactions and non-linearities, which you can either engineer as features or leave to a more flexible model.
Key takeaways
- Least squares has a closed-form solution from the normal equations, but gradient descent scales to problems where it does not.
- Logistic regression models log-odds as a linear function, so each coefficient multiplies the odds by e to the power of that coefficient.
- Log-loss heavily penalises confident wrong predictions and has the simple gradient (prediction minus target) times input.
- Ridge shrinks weights smoothly, lasso sets some exactly to zero, and elastic net combines both; all require standardised features and a tuned strength.
- Softmax with cross-entropy generalises logistic regression to many classes and is the output layer of most neural classifiers.
- Coefficients describe associations conditional on the other features, not causes, and are unstable when features are correlated.
Terms in this lesson
- Least squares
- Choosing model parameters to minimise the sum of squared differences between predictions and targets.
- Normal equations
- The linear system X^T X theta = X^T y whose solution gives the least-squares parameters.
- Learning rate
- The step size multiplying the gradient in each gradient-descent update.
- R-squared
- One minus the ratio of the model's squared error to the squared error of predicting the mean.
- Sigmoid
- The function 1/(1+e^(-z)) that maps any real number to a probability between 0 and 1.
- Log-odds (logit)
- The logarithm of p/(1-p); logistic regression models it as a linear function of the features.
- Log-loss
- The negative average log-probability a model assigns to the true labels, also called binary cross-entropy.
- Regularisation
- A penalty on model complexity, such as the size of the weights, added to the loss to reduce overfitting.
- Softmax
- A function that turns a vector of K scores into K positive probabilities summing to one by exponentiating and normalising.
Check your understanding
- 1.
A logistic regression model has coefficient 0.5 on feature x. What happens when x increases by 2 units, others fixed?
- 2.
You have 200 features, many of them irrelevant, and you want a model that uses only a small subset. Which regulariser best fits this goal?
- 3.
A model predicts p = 0.2 for an example whose true label is 1. What is its log-loss contribution (natural log)?
- 4.
Which statement about the normal equation is correct?
- 5.
Your linear regression has training R^2 = 0.93 and cross-validated R^2 = 0.41. What is the most likely diagnosis and fix?
Exercises
Work each problem before opening the solution.
- Exercise 1Core
Fit a least-squares line by hand to the points . Report , , the residuals, SSE, SST and .
Show solution
Means: , .
Numerator .
Denominator .
, .
Predictions: $1.1, 2.7, 4.3, 5.9y - \hat y-0.1, 0.3, -0.3, 0.1$.
. .
.
- Exercise 2Core
A logistic regression predicts exam success from hours studied: , . (a) At how many hours is ? (b) What is for 7 hours? (c) By what factor do the odds change per extra hour? (d) A student who studied 7 hours failed; what is the log-loss for that example?
Show solution
(a) when , so hours.
(b) ; .
(c) The odds are multiplied by per hour.
(d) True label , so loss - a large loss, because the model was fairly confident and wrong.
- Exercise 3Core
Implement binary logistic regression with batch gradient descent in numpy on the standardised breast cancer dataset (
load_breast_cancer). Report the training log-loss and accuracy, and compare accuracy with scikit-learn'sLogisticRegressionusing a very largeC(almost no regularisation).Show solution
import numpy as np from sklearn.datasets import load_breast_cancer from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression X, y = load_breast_cancer(return_X_y=True) X = StandardScaler().fit_transform(X) n, d = X.shape def sigmoid(z): return 1 / (1 + np.exp(-z)) w, b, lr = np.zeros(d), 0.0, 0.1 for step in range(5000): p = sigmoid(X @ w + b) grad_w = X.T @ (p - y) / n # gradient of mean log-loss grad_b = np.mean(p - y) w -= lr * grad_w b -= lr * grad_b p = np.clip(sigmoid(X @ w + b), 1e-12, 1 - 1e-12) loss = -np.mean(y * np.log(p) + (1 - y) * np.log(1 - p)) print(f"numpy GD: train log-loss {loss:.4f}, accuracy {np.mean((p > 0.5) == y):.3f}") sk = LogisticRegression(C=1e6, max_iter=10000).fit(X, y) # C huge = almost no penalty print(f"sklearn : accuracy {sk.score(X, y):.3f}")A typical run gives a training log-loss of about 0.05 and accuracy about 0.99 for the numpy version, and about 0.99 for scikit-learn. They differ slightly because 5,000 gradient steps have not fully converged, while the nearly unregularised scikit-learn solver pushes further toward the (almost separable) optimum. These are training numbers; on held-out data an unregularised model would typically do no better, and often worse, than the default
C=1. - Exercise 4Challenge
(a) Derive the closed-form ridge solution for centred data with loss . (b) Explain why it exists even when is singular. (c) Implement it in numpy on
load_diabeteswith and verify it matchessklearn.linear_model.Ridge(alpha=10), including the intercept.Show solution
(a) . Gradient: . Setting it to zero: , i.e. , giving . is strictly convex for , so this is the unique minimum.
(b) is positive semi-definite, so its eigenvalues are . Adding raises every eigenvalue by , making the matrix positive definite and therefore invertible, even with collinear features.
(c) Scikit-learn does not penalise the intercept, so centre and , solve for , then recover :
import numpy as np from sklearn.datasets import load_diabetes from sklearn.linear_model import Ridge X, y = load_diabetes(return_X_y=True) alpha = 10.0 Xc = X - X.mean(axis=0) yc = y - y.mean() w = np.linalg.solve(Xc.T @ Xc + alpha * np.eye(X.shape[1]), Xc.T @ yc) b = y.mean() - X.mean(axis=0) @ w sk = Ridge(alpha=alpha).fit(X, y) print(np.allclose(w, sk.coef_), np.isclose(b, sk.intercept_)) # True TrueBoth checks print
True. As a sanity check on shrinkage, the norm of the ridge weight vector here is about 146, versus about 1,378 for the unregularised least-squares solution on the same centred data.
Further reading
- courseTengyu Ma and Andrew Ng, CS229 Lecture Notes (Stanford), ch. 1-2: linear regression, logistic regression, softmax
- bookJames, Witten, Hastie, Tibshirani (and Taylor, Python ed.), An Introduction to Statistical Learning, ch. 3, 4 and 6
- docsscikit-learn developers, sklearn.linear_model API reference and Linear Models user guide
- docsscikit-learn developers, Common pitfalls and recommended practices (inconsistent preprocessing and scaling)
Go further
Sources
Fact-checked and last reviewed 2026-11-05