Machine Learning · Module 1 of 26 · 9 concepts · about 16 min

Machine Learning Fundamentals

What a learned model actually is, the objects it is built from, and why performance on the data you trained on is not the question.

About Machine Learning: Machine learning teaches a computer to make predictions by learning from examples instead of following rules you write by hand. The hard part is not getting a model to run, but knowing whether it will still work on new data it has never seen. You will begin with the basic idea and how to prepare data, then meet the main kinds of models, from simple straight-line models to decision trees and clustering, and learn how to tell if a model is any good. It finishes with what happens after a model is built: explaining it, shipping it and keeping it working.

At work: The ideas behind every prediction a business makes with data: who will churn, which transaction is fraud, what a house will sell for.

What machine learning is

What it is

Instead of writing the rule that maps an input to an output, you supply examples of inputs paired with correct outputs and let an algorithm search for a function that reproduces them. The program is the fitted function; the code you write only describes the search.

Why it matters

It changes where the work goes. In rule-based software the hard part is expressing the logic; in ML the logic is inferred, so the hard parts become the data, the choice of what to predict, and checking that the inferred rule holds on examples it never saw.

In the real world: Instead of hand-writing rules for spam, an email provider shows a model millions of labelled emails and lets it learn the patterns.

Visual example

Supervised learning

Learning turns labelled examples into a function. The examples are the specification; the fitted function is the program.

Training examples (X, y)
n_linkscaps_ratiospam
010.020
170.411
200.050
390.331
fit: search for f
Fitted model f(x)
learned value
w10.62
w23.1
b-3.45

Why this matters

It locates the leverage. Two weeks spent improving the labels in the left-hand table moves the result more than two weeks spent swapping the algorithm that produced the right-hand one.

Common misconception

Wrong idea: the model contains the data it was trained on, and prediction is a lookup.

Correct idea: training compresses the examples into a handful of parameters. The rows are gone; only the fitted function remains, which is exactly why it can answer about rows it never saw — and why it can be wrong about them.

  1. 1Examples inEach row is one input with its known correct answer.
  2. 2SearchThe algorithm adjusts parameters to reduce error on those rows.
  3. 3Function outWhat ships is the parameters, not the table.

The algorithm never sees your intent — only these rows. Whatever pattern is in them is the pattern it learns.

# Hand-coded rule: you state the logic.
def is_spam(subject, n_links):
    return "free" in subject.lower() and n_links > 3

# Learned rule: you state examples, the algorithm finds the logic.
from sklearn.linear_model import LogisticRegression

model = LogisticRegression()
model.fit(X_train, y_train)          # X: features per email, y: 0/1 spam
model.predict(X_test[:5])            # the learned rule, applied

Reaching for ML when a rule would do. If the mapping is known, short, and stable — tax brackets, a validity check — a learned model is strictly worse: it is approximate, it needs data, and it can drift. ML earns its place when the rule is unknown, long, or changing.

Supervised, unsupervised and reinforcement learning

What it is

Supervised learning has a known correct answer per example and learns to reproduce it. Unsupervised learning has inputs only and finds structure — clusters, low-dimensional directions, outliers. Reinforcement learning has no per-example answer at all, only a reward signal for sequences of actions taken in an environment.

Why it matters

The paradigm is decided by the data you actually have, not by the algorithm you like. Asking 'do I have a label for each row?' first rules out most of the search space and stops months being spent looking for a supervised solution to a problem with no labels.

In the real world: Predicting churn from past churners is supervised; grouping customers into segments is unsupervised; tuning ad bids by trial and reward is reinforcement.

Visual example

Learning paradigms

The three paradigms differ by what supervision signal exists, which is a fact about your data, not a choice.

The deciding question

(is there a y?)

Which paradigm your data allows
signal per exampletypical outputhow it is scored
Superviseda correct answerpredicted label or valueerror against held-out answers
Unsupervisednonegroups, components, scoresindirectly, or by a human
Reinforcementdelayed rewarda policy over actionsreturn earned in the environment

Everything in modules 4-7 is supervised

Why this matters

Most confusion about 'which algorithm should I use' dissolves once the paradigm is named, because each paradigm has its own, much shorter, menu.

Common misconception

Wrong idea: unsupervised learning is supervised learning without the effort of labelling.

Correct idea: it answers a different question. Without labels there is nothing to be accurate about, so its results have to be validated by downstream use or by a human reading them.

from sklearn.linear_model import LinearRegression
from sklearn.cluster import KMeans

LinearRegression().fit(X, y)     # supervised: X and y
KMeans(n_clusters=4).fit(X)      # unsupervised: X only, no y argument

# The signature tells you which it is: a fit() that takes no y is unsupervised.

Treating clustering output as if it were a label. K-means will happily return four groups from pure noise; the groups have no ground truth to be right or wrong against, so 'the clusters look good' is an opinion, not a measurement.

Features, labels, and the design matrix X / y

What it is

The conventional layout is one row per example and one column per feature: X is 2-D with shape (n_samples, n_features), and the target y is 1-D with shape (n_samples,). Row i of X and element i of y are the same example.

Why it matters

Every scikit-learn estimator assumes exactly this, and mismatched shapes are the single most common beginner error. It also makes the alignment rule explicit: anything that reorders X must reorder y identically or every label is attached to the wrong row.

In the real world: For a house-price model, each row is a house, the columns are size, location and age (X), and the sale price is the label (y).

Visual example

Design matrix

One row is one example; one column is one feature. y is a single column aligned to the rows.

One example

(a row of X plus its label)

One feature

(a column of X)

X (3, 2) and y (3,)
ageincomey
025500000
141820001
233610001

X[1]

shape (2,) — reshape(1, -1) to predict on it

y

shape (3,) — one label per row, same order

X.shape[0] == y.shape[0] always

Shapes, stated

X.shape -> (3, 2): 3 samples, 2 features

y.shape -> (3,): 1-D, never (3, 1)

X[:, 0].shape -> (2,)? No: (3,) — one column, all rows

Why this matters

Half of the errors scikit-learn raises are this picture being transposed or flattened. Reading X as rows-of-examples makes the fix obvious instead of a guess between reshape(1, -1) and reshape(-1, 1).

Common misconception

Wrong idea: X and y are just 'the data' and the library will figure out the orientation.

Correct idea: the orientation is a contract: rows are samples. A transposed X fits happily and learns nonsense, because 500 features and 4 samples is a legal matrix.

import numpy as np

X = np.array([[25, 50_000.0], [41, 82_000.0], [33, 61_000.0]])   # (3, 2)
y = np.array([0, 1, 1])                                          # (3,)
X.shape, y.shape

X_one = X[0].reshape(1, -1)      # (1, 2): one sample, not one feature
X_col = X[:, 0].reshape(-1, 1)   # (3, 1): one feature as a column

Passing a 1-D array as X. model.predict(X[0]) raises, because a (2,) array is ambiguous: it could be one sample with two features or two samples with one. Say which with reshape(1, -1) or reshape(-1, 1) — the same (n,) versus (n, 1) distinction the NumPy shapes module drilled.

Parameters versus hyperparameters

What it is

Parameters are the numbers the fitting procedure learns from the data — coefficients, intercepts, split thresholds. Hyperparameters are the numbers you choose before fitting, which control the search itself: regularisation strength, tree depth, learning rate, k.

Why it matters

They are validated in completely different ways. Parameters are fitted on the training set; hyperparameters must be chosen using data the fit did not see, or you are selecting the setting that best memorises your training rows.

In the real world: A churn model learns how much “months since last login” matters (a parameter); the team chooses how deep its trees can grow (a hyperparameter).

Visual example

scikit-learn API

The trailing underscore is the convention: **coef_ was learned**, alpha was chosen.

Trailing underscore

(exists only after fit())

Ridge(alpha=1.0).fit(X_train, y_train)
kindset bychosen using
alphahyperparameteryou, before fitvalidation set
fit_intercepthyperparameteryou, before fitdomain knowledge
coef_parameterfit(), from datatraining set
intercept_parameterfit(), from datatraining set

get_params() lists the top two; only fit creates the bottom two

Why this matters

It tells you which split a number is allowed to look at. Parameters may see the training set; hyperparameters may not, not even once, or the honest estimate is gone.

Common misconception

Wrong idea: hyperparameters are just settings, so trying values against the test score is harmless.

Correct idea: choosing a hyperparameter is fitting, with you as the optimiser. It needs its own held-out data exactly as parameter fitting does.

from sklearn.linear_model import Ridge

model = Ridge(alpha=1.0)      # alpha is a hyperparameter: chosen by you
model.fit(X_train, y_train)
model.coef_                   # parameters: learned from the data
model.intercept_
model.get_params()            # hyperparameters only — never coef_

Tuning hyperparameters against the test set. Each time you look at the test score and change alpha, a little test information leaks into the model; after twenty rounds the test score is an optimistic estimate of nothing. That is what the validation set exists for.

The train / validation / test split

What it is

Three disjoint subsets with three different jobs: the training set fits parameters, the validation set chooses hyperparameters and compares models, and the test set is touched once, at the end, to estimate performance on unseen data.

Why it matters

Without the middle set there is nowhere honest to make choices. With it, the test set stays genuinely unseen, which is the only condition under which its number means 'what I should expect in production'.

In the real world: A bank trains a credit model on older loans, tunes it on some newer ones, and keeps the newest aside for one honest final check.

Visual example

Data splitting

Three subsets, three jobs. The test set's value comes entirely from never having been consulted.

Locked away

(look once, report once)

Where choices happen

(all model selection)

One dataset, split three ways
shareits jobhow often you look
Train60%fit parametersevery epoch
Validation20%pick hyperparameters, compare modelsevery experiment
Test20%estimate unseen performanceonce, at the end

Split before computing any statistic from the data

Why the validation score drifts optimistic

20 configurations tried, each scored on the same 200 validation rows

best-of-20 on noisy scores is biased upward by roughly one standard error

the test set was not part of that maximum, so it is not biased

Why this matters

It is the whole reason an offline number can be trusted. Every leakage bug in the next module is this picture being violated by a statistic computed too early.

Common misconception

Wrong idea: two splits are enough — train and test.

Correct idea: two splits are enough only if you make no choices. The moment you compare two models you need a third subset, or the test set has quietly become a validation set.

from sklearn.model_selection import train_test_split

# Hold out the test set first, and then split the rest.
X_rest, X_test, y_rest, y_test = train_test_split(
    X, y, test_size=0.2, random_state=0, stratify=y
)
X_train, X_val, y_train, y_val = train_test_split(
    X_rest, y_rest, test_size=0.25, random_state=0, stratify=y_rest
)
len(X_train), len(X_val), len(X_test)     # 60% / 20% / 20% of the original

Reporting the best validation score as the final result. The validation score is optimistic by construction — it is the maximum over everything you tried, so it has absorbed the noise of the winning run. Quote the test score, and quote it once.

Practise it: A Pipeline That Does Not Leak, A Split That Does Not Leak Across Groups

The fit / predict / score loop

What it is

Every scikit-learn estimator exposes the same three-call shape: fit(X, y) learns parameters and returns the estimator, predict(X) maps new rows to outputs, and score(X, y) returns a default metric (R² for regressors, accuracy for classifiers).

Why it matters

Because the interface is identical across dozens of models, swapping a linear model for a gradient-boosted one is a one-line change. That uniformity is what makes a baseline cheap, and a cheap baseline is what keeps the rest of the project honest.

In the real world: Train a delivery-time model on last year's orders, predict today's deliveries, and score how close the predictions were.

from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression

for model in [LogisticRegression(max_iter=1000), RandomForestClassifier(random_state=0)]:
    model.fit(X_train, y_train)
    print(type(model).__name__, model.score(X_val, y_val))

# Transformers use the same shape with transform() instead of predict().

Trusting score() without knowing what it computes. It is accuracy for classifiers, which is nearly meaningless on imbalanced data, and R² for regressors, which can be negative. Name the metric you care about explicitly rather than accepting the default.

Practise it: Macro and Micro F1, From the Confusion Matrix Alone

Loss functions: what is actually being optimised

What it is

A loss function turns one prediction and one true value into a single number measuring how bad that prediction was. Fitting minimises the average loss over the training set, so the loss — not your intention — defines what 'good' means.

Why it matters

The loss shape decides the model's behaviour. Squared error grows quadratically with the residual, so one point ten units away matters as much as a hundred points one unit away; absolute error does not, and the fitted line differs accordingly.

In the real world: A demand forecast that punishes big misses heavily avoids the costly stock-outs that small errors never cause.

Visual example

Loss functions

The loss shape is the priority list. Squared error punishes a single large miss far harder than absolute error does.

Squared errorAbsolute errorHuber (delta = 1)
-4-202402468Residual (y_true - y_pred)Loss for that one exampleperfect predictionone miss of 3 costs 9

Ten residuals of 1 versus one residual of 10

squared: 10 x 1^2 = 10 vs 1 x 10^2 = 100 -> the single outlier wins

absolute: 10 x 1 = 10 vs 1 x 10 = 10 -> they tie

so MSE moves the fit toward the outlier; MAE barely notices it

Why this matters

Every 'why did the model do that?' about a regression fit is usually answered by this curve: the loss told it that one far-away point mattered more than a crowd of near ones.

Common misconception

Wrong idea: the loss is an implementation detail of the optimiser.

Correct idea: the loss is the objective. Changing it changes the model you get from identical data, which is why picking it is a modelling decision, not a default to accept.

Both losses are zero at a perfect prediction and rise on either side. Only the rate differs — and the rate is what the optimiser responds to.

import numpy as np

resid = y_true - y_pred
mse = np.mean(resid ** 2)                      # squared error: outlier-sensitive
mae = np.mean(np.abs(resid))                   # absolute error: robust

delta = 1.0                                     # Huber: quadratic near 0, linear far out
small = np.abs(resid) <= delta
huber = np.mean(np.where(small, 0.5 * resid ** 2,
                         delta * (np.abs(resid) - 0.5 * delta)))

Optimising squared error and then complaining the model is dragged around by outliers. That is not a bug; it is precisely what you asked for. If large residuals should not dominate, change the loss (MAE, Huber) rather than deleting the inconvenient rows.

Generalization: why training error is not the goal

What it is

The goal is low error on data drawn from the same distribution but never seen during fitting. Training error can always be driven to zero by a model with enough capacity to memorise, and a memoriser has learned nothing transferable.

Why it matters

It is the reason every practice in this syllabus exists — held-out splits, cross-validation, regularisation, early stopping. All of them are devices for estimating or improving the error you cannot directly measure.

In the real world: A fraud model that is perfect on last year's data but misses this month's new fraud patterns is worthless to the bank.

Visual example

Generalization

A perfect training score is free for a flexible model — and tells you nothing about unseen rows.

Perfect, and useless

(1-NN memorises)

The only honest column

(unseen rows)

Same data, two models
train accuracytest accuracygap
1-NN (memoriser)10.710.29
25-NN (smoothed)0.860.840.02
Majority baseline0.520.510.01

Pick on the test column, not the train column

Reading the table

1-NN: train 1.00, test 0.71 -> gap 0.29, a textbook overfit

25-NN: train 0.86, test 0.84 -> small gap, and the better test score

a large gap means variance; a low train score means bias (module 7)

Why this matters

It reframes the whole task: you are not fitting the data you have, you are estimating performance on data you do not have. Every technique later on is a tool for that estimate.

Common misconception

Wrong idea: a model that fits the training data perfectly is the best model.

Correct idea: perfect training fit is a warning sign. The quantity you want — error on unseen rows — is usually worse for the perfect fitter than for the modest one.

from sklearn.neighbors import KNeighborsClassifier

memoriser = KNeighborsClassifier(n_neighbors=1).fit(X_train, y_train)
memoriser.score(X_train, y_train)     # 1.0 — every point is its own neighbour
memoriser.score(X_test, y_test)       # much lower: nothing was generalised

smooth = KNeighborsClassifier(n_neighbors=25).fit(X_train, y_train)
smooth.score(X_train, y_train), smooth.score(X_test, y_test)

Celebrating a training accuracy of 1.0. On any non-trivial problem it is evidence of memorisation, not of skill. The only number worth reporting is one computed on rows the fit never touched.

The i.i.d. assumption, and when it fails

What it is

Standard supervised learning assumes training and future rows are independent draws from one fixed distribution. Random splits, cross-validation and every offline metric inherit that assumption.

Why it matters

When it fails, offline numbers stop predicting live behaviour while still looking excellent. Time series, grouped data (many rows per user) and shifting populations all break it, and each needs a split that respects the structure.

In the real world: A sales model trained before a festival season assumes the future looks like the past, and breaks when Diwali demand arrives.

from sklearn.model_selection import GroupKFold, TimeSeriesSplit

# Many rows per user: keep a user entirely on one side of the split.
GroupKFold(n_splits=5).split(X, y, groups=user_ids)

# Time-ordered data: train on the past, validate on the future.
TimeSeriesSplit(n_splits=5).split(X)

Using a random split on time-ordered data. Rows from next month land in the training set, the model learns from the future, and the validation score is far better than anything achievable in production — where the future is, inconveniently, not available.