Chapter 2 — Notation and Definitions¶

Before we build any real models, we need a shared vocabulary. This chapter collects the notation and a handful of core ideas — vectors, random variables, Bayes' rule, and the difference between classification and regression — that every later chapter leans on. We'll keep the math light and ground each idea in a tiny runnable Python example.

In this chapter you will learn:

  • How to read the basic notation: scalars, vectors, matrices, sets, and the real numbers $\mathbb{R}$
  • What a dot product and a vector norm are, and the difference between the 1-norm and the 2-norm
  • What a random variable is, and the difference between a probability mass function (PMF) and a probability density function (PDF)
  • What an "unbiased estimator" means and why the sample mean is one
  • How to use Bayes' rule to flip a conditional probability around
  • The ideas behind parameter estimation: maximum likelihood (MLE) and MAP
  • The difference between classification and regression
  • The difference between model-based and instance-based learning
  • The difference between shallow and deep learning

2.1 Notation — Scalars, Vectors, and Sets¶

A scalar is just a single number, like $15$ or $-3.25$. We write scalars as ordinary italic letters: $x$, $a$, $c$.

A vector is an ordered list of scalars. We write vectors in bold, like $\mathbf{x}$ or $\mathbf{w}$. You can picture a vector two ways: as an arrow pointing in a direction, or as a point sitting at a location in space. The individual numbers inside the vector are its attributes (or components), and we pick one out with an index: $x^{(j)}$ means "the $j$-th attribute of $\mathbf{x}$." (Don't confuse this with a power like $x^2$ — to square an attribute we'd write $(x^{(j)})^2$.)

Everyday analogy: a vector is like a row in a spreadsheet. Each column is an attribute, and the whole row is one example.

A matrix is a grid of numbers (a table of vectors). We usually give matrices capital letters like $W$. A matrix with 2 rows and 3 columns has shape $2 \times 3$.

A set is an unordered collection of unique things, written with a calligraphic capital like $\mathcal{S}$. A finite set uses braces: $\{1, 3, 18\}$. The set of all real numbers — everything from $-\infty$ to $+\infty$ — gets the special symbol $\mathbb{R}$. When something $x$ belongs to a set $\mathcal{S}$ we write $x \in \mathcal{S}$.

The two stars of supervised learning are the feature vector $\mathbf{x}$ (the inputs describing an example) and the label $y$ (the thing we want to predict). A whole dataset of $N$ examples is often written $\{(\mathbf{x}_i, y_i)\}_{i=1}^{N}$.

In [1]:
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt

plt.rcParams["figure.figsize"] = (6, 4)
plt.rcParams["axes.grid"] = True

# Three 2D vectors (like the book's example)
a = np.array([2, 3])
b = np.array([-2, 5])
c = np.array([1, 0])

# A tiny matrix: 2 rows x 3 columns
W = np.array([[1, 2, 3],
              [4, 5, 6]])

print("vector a =", a, "   a[0] =", a[0], "(the 1st attribute)")
print("vector b =", b)
print("vector c =", c)
print("matrix W (2x3) =\n", W)
print("W.shape =", W.shape, "-> 2 rows, 3 columns")

# Plot the same vectors two ways: as arrows AND as points at their tips
fig, ax = plt.subplots()
origin = np.array([0, 0])
for vec, name, col in [(a, "a = [2, 3]", "tab:red"),
                       (b, "b = [-2, 5]", "tab:blue"),
                       (c, "c = [1, 0]", "tab:green")]:
    ax.annotate("", xy=vec, xytext=origin,
                arrowprops=dict(arrowstyle="->", color=col, lw=2))  # arrow view
    ax.scatter(*vec, color=col, zorder=5)                            # point view
    ax.text(vec[0] + 0.1, vec[1] + 0.1, name, color=col, fontsize=11)
ax.axhline(0, color="gray", lw=0.5)
ax.axvline(0, color="gray", lw=0.5)
ax.set_xlim(-3, 4)
ax.set_ylim(-1, 6)
ax.set_title("Vectors shown as arrows (and their tips as points)")
ax.set_xlabel("dimension 1")
ax.set_ylabel("dimension 2")
plt.show()
vector a = [2 3]    a[0] = 2 (the 1st attribute)
vector b = [-2  5]
vector c = [1 0]
matrix W (2x3) =
 [[1 2 3]
 [4 5 6]]
W.shape = (2, 3) -> 2 rows, 3 columns
No description has been provided for this image

Reading the plot¶

Each vector appears twice on purpose: once as an arrow from the origin, and once as a point at the arrow's tip. Both pictures are valid and we'll switch between them throughout the book — arrows are handy for thinking about direction, points are handy for thinking about data. The matrix W above is just a 2×3 grid of numbers; in code, W.shape tells us "2 rows, 3 columns."

2.1 (continued) — Dot Product and Norms¶

The dot product (a.k.a. inner product or scalar product) of two same-length vectors $\mathbf{w}$ and $\mathbf{x}$ is a single number:

$$\mathbf{w}\,\mathbf{x} \;=\; \sum_{i=1}^{m} w^{(i)} x^{(i)} \;=\; w^{(1)}x^{(1)} + w^{(2)}x^{(2)} + \dots + w^{(m)}x^{(m)}.$$

It's a way to "combine" two vectors into one number. A large positive dot product tends to mean the vectors point in a similar direction and are long; a dot product near zero often means they're roughly perpendicular.

A norm $\|\mathbf{x}\|$ measures a vector's "length." The two you'll meet most are:

  • 2-norm (Euclidean length — the everyday straight-line distance):
$$\|\mathbf{x}\|_2 = \sqrt{\sum_i \bigl(x^{(i)}\bigr)^2}$$
  • 1-norm (sum of absolute values — "taxicab" distance):
$$\|\mathbf{x}\|_1 = \sum_i \bigl|x^{(i)}\bigr|$$

Analogy: the 2-norm is how a bird flies from A to B (a straight line). The 1-norm is how a taxi drives through a city grid (only horizontal and vertical moves).

In [2]:
# Dot product: combine two same-length vectors into one scalar
w = np.array([1, 2, 3])
x = np.array([4, 5, 6])

dot = np.dot(w, x)            # 1*4 + 2*5 + 3*6
print("w =", w)
print("x =", x)
print("dot product w·x =", dot, "  (manual check:", 1*4 + 2*5 + 3*6, ")")

# Norms: measure a vector's "length"
v = np.array([3, 4])
l2 = np.linalg.norm(v)                 # default = 2-norm: sqrt(3^2 + 4^2) = 5
l1 = np.linalg.norm(v, ord=1)          # 1-norm: |3| + |4| = 7
print("\nvector v =", v)
print("2-norm (Euclidean) ||v||_2 =", l2)
print("1-norm (taxicab)    ||v||_1 =", l1)

# Visual: a straight-line walk vs a grid walk for v = [3, 4]
fig, ax = plt.subplots()
ax.plot([0, v[0]], [0, v[1]], color="tab:blue", lw=2, label="2-norm path (straight)")
ax.plot([0, v[0], v[0]], [0, 0, v[1]], color="tab:orange", lw=2, ls="--",
        label="1-norm path (taxicab)")
ax.scatter(*v, color="black", zorder=5)
ax.text(v[0] + 0.1, v[1] + 0.1, "v = [3, 4]", fontsize=11)
ax.set_xlim(-0.5, 4.5)
ax.set_ylim(-0.5, 5)
ax.set_title("Two ways to measure the length of v = [3, 4]")
ax.set_xlabel("dimension 1")
ax.set_ylabel("dimension 2")
ax.legend()
plt.show()
w = [1 2 3]
x = [4 5 6]
dot product w·x = 32   (manual check: 32 )

vector v = [3 4]
2-norm (Euclidean) ||v||_2 = 5.0
1-norm (taxicab)    ||v||_1 = 7.0
No description has been provided for this image

Reading the output¶

The dot product of $\mathbf{w}=[1,2,3]$ and $\mathbf{x}=[4,5,6]$ is $1\cdot4 + 2\cdot5 + 3\cdot6 = 32$. For $\mathbf{v}=[3,4]$, the 2-norm is $5$ (the straight-line length, matching the Pythagorean triple 3-4-5), while the 1-norm is $7$ (the longer taxi route: 3 across + 4 up). Same vector, two different notions of "length" — both show up in machine learning.

2.2 Random Variables¶

A random variable (written as a capital italic letter like $X$) is a variable whose values come from some random process. There are two flavors:

  • Discrete: takes a countable set of values (a die roll: 1–6; or a color: red / yellow / blue).
  • Continuous: takes values anywhere in an interval (height, weight, time).

A random variable's behavior is described by its probability distribution:

  • For a discrete variable we use a probability mass function (PMF) — a list like $\Pr(X=\text{red})=0.3$, $\Pr(X=\text{yellow})=0.45$, $\Pr(X=\text{blue})=0.25$. Every probability is $\ge 0$ and they all sum to $1$.
  • For a continuous variable we use a probability density function (PDF) — a curve where the area under the curve in any region gives the probability of landing there. The total area under the whole curve is $1$. (For any single exact value the probability is $0$, which is why we talk about areas, not heights.)

Two summary numbers describe a distribution:

  • The mean (also called the expected value or expectation), written $\mu = \mathbb{E}[X]$, is the "center of mass." For a discrete variable: $\mathbb{E}[X] = \sum_i x_i \Pr(X=x_i)$.
  • The variance $\text{Var}(X) = \mathbb{E}\bigl[(X-\mu)^2\bigr]$ (and its square root, the standard deviation $\sigma$) measures how spread out the values are.

Analogy: the mean is where the distribution would balance on your fingertip; the standard deviation is how wide it wobbles.

Most of the time we don't know the true distribution — we only see a sample (a dataset) of observed values. The next two sections build on exactly that situation.

In [3]:
# A continuous random variable: X ~ Normal(mean=5, std=2)
true_mean, true_std = 5.0, 2.0
rng = np.random.default_rng(42)

# Draw a sample of 5000 values
sample = rng.normal(loc=true_mean, scale=true_std, size=5000)

fig, axes = plt.subplots(1, 2, figsize=(11, 4))

# Left: a histogram of the samples approximates the PDF
axes[0].hist(sample, bins=40, density=True, color="tab:blue", alpha=0.7)
axes[0].axvline(true_mean, color="tab:red", ls="--", label=f"true mean = {true_mean}")
axes[0].set_title("Histogram of samples (approximates the PDF)")
axes[0].set_xlabel("value")
axes[0].set_ylabel("density")
axes[0].legend()

# Right: the running sample mean homes in on the true mean (Law of Large Numbers)
n_vals = np.arange(1, len(sample) + 1)
running_mean = np.cumsum(sample) / n_vals
axes[1].plot(n_vals, running_mean, color="tab:blue", lw=1, label="sample mean so far")
axes[1].axhline(true_mean, color="tab:red", ls="--", label=f"true mean = {true_mean}")
axes[1].set_title("Sample mean approaches the true mean as n grows")
axes[1].set_xlabel("number of samples so far (n)")
axes[1].set_ylabel("running mean")
axes[1].legend()
plt.tight_layout()
plt.show()

print(f"Sample mean over all 5000 draws: {sample.mean():.3f}   (true mean: {true_mean})")
print(f"Sample std : {sample.std():.3f}   (true std : {true_std})")
No description has been provided for this image
Sample mean over all 5000 draws: 4.960   (true mean: 5.0)
Sample std : 1.999   (true std : 2.0)

Reading the plots¶

On the left, the histogram is a rough, blocky picture of the bell-shaped PDF — the more samples we draw, the closer the bars hug the true curve. On the right, the running sample mean bounces around wildly at first (a single draw can drag it far from 5) but settles onto the true mean $5$ as $n$ grows. This settling is the Law of Large Numbers, and it's the reason datasets work: with enough examples, sample statistics reveal the true distribution.

2.3 Unbiased Estimators¶

Since we usually can't see the true distribution, we estimate its statistics from a sample $\mathcal{S}_X = \{x_1, \dots, x_N\}$. An estimator $\hat{\theta}$ is a formula that turns a sample into a guess for some statistic $\theta$ (such as the mean $\mu$).

We call $\hat{\theta}$ an unbiased estimator of $\theta$ if, averaged over all possible samples, it lands on the true value:

$$\mathbb{E}\bigl[\hat{\theta}(\mathcal{S}_X)\bigr] = \theta.$$

In plain words: the estimator isn't systematically too high or too low. If you could draw infinitely many samples and average their $\hat{\theta}$'s, you'd hit $\theta$ exactly.

Good news: the sample mean $\hat{\mu} = \frac{1}{N}\sum_{i=1}^N x_i$ is an unbiased estimator of the true mean. A subtler point: the sample variance is only unbiased when you divide by $N-1$, not $N$. Dividing by $N$ gives a value that is systematically too small — a biased estimator. Let's see both effects in code.

In [4]:
# Show the sample MEAN is unbiased, and that variance is biased (÷N) vs unbiased (÷N-1)
true_mean, true_std = 5.0, 2.0
true_var = true_std ** 2
rng = np.random.default_rng(7)

N = 10                 # each sample is small
repeats = 8000         # but we take many of them
sample_means = np.empty(repeats)
biased_vars = np.empty(repeats)
unbiased_vars = np.empty(repeats)

for r in range(repeats):
    s = rng.normal(loc=true_mean, scale=true_std, size=N)
    sample_means[r] = s.mean()
    biased_vars[r] = s.var(ddof=0)       # divide by N      -> biased
    unbiased_vars[r] = s.var(ddof=1)     # divide by (N-1)  -> unbiased

print(f"true mean = {true_mean},   true variance = {true_var}")
print(f"avg of sample means       = {sample_means.mean():.4f}   (unbiased: matches true mean)")
print(f"avg of biased  variance   = {biased_vars.mean():.4f}   (too low)")
print(f"avg of unbiased variance  = {unbiased_vars.mean():.4f}   (matches true variance)")

fig, ax = plt.subplots()
ax.hist(sample_means, bins=40, density=True, color="tab:blue", alpha=0.7)
ax.axvline(true_mean, color="tab:red", ls="--", label=f"true mean = {true_mean}")
ax.axvline(sample_means.mean(), color="tab:green", ls=":",
           label=f"avg sample mean = {sample_means.mean():.3f}")
ax.set_title("Distribution of the sample mean over many small samples")
ax.set_xlabel("sample mean value")
ax.set_ylabel("density")
ax.legend()
plt.show()
true mean = 5.0,   true variance = 4.0
avg of sample means       = 4.9972   (unbiased: matches true mean)
avg of biased  variance   = 3.5897   (too low)
avg of unbiased variance  = 3.9886   (matches true variance)
No description has been provided for this image

Reading the output¶

Even though each individual sample of size 10 has a noisy mean, the average of all 8000 sample means lands right on 5 — that's what "unbiased" means. For variance, the picture is different: dividing by $N$ systematically underestimates the spread (you'll see a number below 4), while dividing by $N-1$ corrects it back to the true variance of 4. This is why libraries like numpy let you choose ddof ("delta degrees of freedom").

2.4 Bayes' Rule¶

Conditional probability $\Pr(X=x \mid Y=y)$ reads as "the probability that $X=x$ given that $Y=y$ has already happened." Bayes' rule lets you flip that condition around:

$$\Pr(X=x \mid Y=y) = \frac{\Pr(Y=y \mid X=x)\,\Pr(X=x)}{\Pr(Y=y)}.$$

People remember it as: posterior = (likelihood × prior) / evidence. We often drop the denominator and just write the proportionality:

$$\Pr(y \mid x) \;\propto\; \Pr(x \mid y)\,\Pr(y).$$

This is enormously useful when it's easier to measure "how likely is this evidence if the cause were $y$" than to measure "how likely is the cause $y$ given the evidence" directly — Bayes' rule bridges that gap.

Analogy: you hear footsteps at night (evidence). Footsteps are very likely if your roommate is home (high likelihood) and your roommate is usually home (high prior), but unlikely for a burglar — so Bayes' rule says "probably the roommate."

In [5]:
# The classic base-rate example: a rare disease and a pretty good test
p_disease = 0.01                       # prior: 1% of people have the disease
p_no_disease = 1 - p_disease
p_pos_given_disease = 0.99             # sensitivity (true positive rate)
p_pos_given_no_disease = 1 - 0.95      # 1 - specificity = false positive rate (5%)

# Evidence: overall probability of getting a positive test
p_positive = (p_pos_given_disease * p_disease
              + p_pos_given_no_disease * p_no_disease)

# Bayes' rule: probability you actually have the disease given a positive test
p_disease_given_pos = (p_pos_given_disease * p_disease) / p_positive

print("Given a POSITIVE test:")
print(f"  P(disease)        = {p_disease:.2f}       (prior)")
print(f"  P(+ | disease)    = {p_pos_given_disease:.2f}       (likelihood / sensitivity)")
print(f"  P(+)              = {p_positive:.4f}    (evidence)")
print(f"  P(disease | +)    = {p_disease_given_pos:.4f}  <-- posterior")
print(f"\nSo a positive test means only a {p_disease_given_pos*100:.1f}% chance you're actually sick.")

fig, ax = plt.subplots(figsize=(5, 3.5))
ax.bar(["prior\nP(disease)", "posterior\nP(disease | +)"],
       [p_disease, p_disease_given_pos], color=["tab:gray", "tab:red"])
ax.set_ylim(0, 1)
ax.set_ylabel("probability")
ax.set_title("Bayes' rule: a 1% prior becomes a 16% posterior")
for i, v in enumerate([p_disease, p_disease_given_pos]):
    ax.text(i, v + 0.02, f"{v*100:.1f}%", ha="center")
plt.show()
Given a POSITIVE test:
  P(disease)        = 0.01       (prior)
  P(+ | disease)    = 0.99       (likelihood / sensitivity)
  P(+)              = 0.0594    (evidence)
  P(disease | +)    = 0.1667  <-- posterior

So a positive test means only a 16.7% chance you're actually sick.
No description has been provided for this image

Reading the result¶

The posterior is only about 16%, even with a test that's 99% sensitive and 95% specific! That feels shocking, but Bayes' rule explains it: because the disease is rare (1% prior), most positive tests come from the large healthy population producing false positives. Forgetting the prior and over-trusting the test is called the base-rate fallacy — and it's exactly the trap Bayes' rule helps us avoid.

2.5 Parameter Estimation — MLE and MAP¶

Often we assume the data comes from a known family of distributions (say a Gaussian) but we don't know its parameters $\theta$ (e.g. $\mu$ and $\sigma$). Parameter estimation is the job of guessing good values of $\theta$ from data.

  • Maximum Likelihood Estimation (MLE): pick the $\theta$ that makes the observed data most probable:
$$\theta_{\text{MLE}} = \arg\max_\theta \prod_{i=1}^N \Pr(x_i \mid \theta).$$

We usually take logs and maximize the log-likelihood instead — it turns the product into a sum (easier math) and avoids multiplying many tiny probabilities into a number too small for the computer to store.

  • Maximum A Posteriori (MAP): like MLE, but we also hold a prior belief $\Pr(\theta)$ about which parameters are plausible. We pick the $\theta$ that maximizes the posterior:
$$\theta_{\text{MAP}} = \arg\max_\theta \;\Pr(x \mid \theta)\,\Pr(\theta).$$

With a flat (uniform) prior, MAP collapses to MLE. With a strong prior, MAP pulls the estimate toward what we believed before seeing any data.

Analogy: MLE trusts only the data. MAP trusts the data and your prior hunch, blended together. With almost no data, the prior dominates; with tons of data, the data dominates and the two agree.

In [6]:
# Estimate the mean of a Gaussian via MLE vs MAP
true_mean, true_std = 7.0, 2.0
rng = np.random.default_rng(3)
N = 5                                      # a TINY sample, so the prior will matter
data = rng.normal(loc=true_mean, scale=true_std, size=N)

# MLE for the mean of a Gaussian = the sample mean
mle = data.mean()

# MAP with a Gaussian prior on the mean: prior ~ N(prior_mean, prior_sd^2)
prior_mean, prior_sd = 0.0, 3.0
# For a Gaussian likelihood + Gaussian prior, the MAP estimate is a
# precision-weighted average of the MLE and the prior mean:
lik_prec = N / (true_std ** 2)             # how strongly the data speaks
prior_prec = 1 / (prior_sd ** 2)           # how strongly the prior speaks
map_est = (lik_prec * mle + prior_prec * prior_mean) / (lik_prec + prior_prec)

print(f"true mean             = {true_mean}")
print(f"sample (N={N})         = {np.round(data, 2)}")
print(f"MLE mean (data only)  = {mle:.3f}")
print(f"MAP mean (+ prior)    = {map_est:.3f}   (pulled toward prior mean {prior_mean})")

# Plot the log-likelihood and log-posterior across candidate means
grid = np.linspace(-2, 10, 300)
loglik = -0.5 * np.sum((data[None, :] - grid[:, None]) ** 2 / true_std ** 2, axis=1)
logpost = loglik - 0.5 * ((grid - prior_mean) ** 2 / prior_sd ** 2)

fig, ax = plt.subplots()
ax.plot(grid, loglik - loglik.max(), color="tab:blue", label="log-likelihood (MLE)")
ax.plot(grid, logpost - logpost.max(), color="tab:orange", label="log-posterior (MAP)")
ax.axvline(mle, color="tab:blue", ls="--", label=f"MLE = {mle:.2f}")
ax.axvline(map_est, color="tab:orange", ls="--", label=f"MAP = {map_est:.2f}")
ax.axvline(prior_mean, color="tab:green", ls=":", label=f"prior mean = {prior_mean}")
ax.set_title("MLE vs MAP for the mean of a Gaussian (tiny sample)")
ax.set_xlabel("candidate mean μ")
ax.set_ylabel("log value (normalized)")
ax.legend()
plt.show()
true mean             = 7.0
sample (N=5)         = [11.08  1.89  7.84  5.86  6.09]
MLE mean (data only)  = 6.553
MAP mean (+ prior)    = 6.018   (pulled toward prior mean 0.0)
No description has been provided for this image

Reading the plot¶

The blue curve (log-likelihood) peaks at the MLE, which is just the sample mean of our five draws. The orange curve (log-posterior) is the likelihood tilted by the prior centered at $0$, so its peak — the MAP estimate — sits between the sample mean and the prior mean. With only five data points the prior still has real pull; collect hundreds of points and the two curves would peak in almost the same spot, because the data would overwhelm the prior.

2.6 Classification vs Regression¶

Supervised learning splits into two big families depending on what the label $y$ looks like:

Classification Regression
Label type a category from a finite set a real number
Example spam / not-spam; dog / cat / bird house price; temperature; wait time
Output a class label (or a probability per class) a continuous value
Variants binary (2 classes) or multiclass (3+) —

When there are exactly two classes it's binary classification; with three or more it's multiclass classification.

Analogy: classification answers "which bucket?" (sorting mail into bins). Regression answers "how much?" (appraising a house's price).

In [7]:
from sklearn.linear_model import LogisticRegression, LinearRegression
from sklearn.datasets import make_classification, make_regression

SEED = 42

# --- Classification: labels are categories (0 / 1) ---
Xc, yc = make_classification(n_samples=120, n_features=2, n_redundant=0,
                             n_informative=2, n_clusters_per_class=1,
                             random_state=SEED)
clf = LogisticRegression(max_iter=200)
clf.fit(Xc, yc)

# --- Regression: labels are real numbers ---
Xr, yr = make_regression(n_samples=120, n_features=1, noise=15.0, random_state=SEED)
reg = LinearRegression()
reg.fit(Xr, yr)

fig, axes = plt.subplots(1, 2, figsize=(11, 4))

# Left: classification with a straight decision boundary
axes[0].scatter(Xc[:, 0][yc == 0], Xc[:, 1][yc == 0], color="tab:blue", label="class 0")
axes[0].scatter(Xc[:, 0][yc == 1], Xc[:, 1][yc == 1], color="tab:orange", label="class 1")
xx = np.linspace(Xc[:, 0].min(), Xc[:, 0].max(), 50)
w0, w1 = clf.coef_[0]
b = clf.intercept_[0]
axes[0].plot(xx, -(w0 * xx + b) / w1, "k-", label="decision boundary")
axes[0].set_title("Classification: LogisticRegression (label = category)")
axes[0].set_xlabel("feature 1")
axes[0].set_ylabel("feature 2")
axes[0].legend()

# Right: regression with a fitted line
axes[1].scatter(Xr.ravel(), yr, color="tab:blue", alpha=0.7, label="data")
axes[1].plot(Xr.ravel(), reg.predict(Xr), color="tab:red", lw=2, label="fitted line")
axes[1].set_title("Regression: LinearRegression (label = number)")
axes[1].set_xlabel("feature")
axes[1].set_ylabel("target (real number)")
axes[1].legend()
plt.tight_layout()
plt.show()
No description has been provided for this image

Reading the plots¶

On the left, logistic regression draws a decision boundary — a line that separates "class 0" from "class 1"; new points are labeled by which side of the line they fall on. On the right, linear regression fits a line through the cloud of points so it can output a real number for any input. Same general idea (learn from labeled examples), but the kind of answer differs: a category versus a quantity.

2.7 Model-Based vs Instance-Based Learning¶

  • Model-based learning uses the training data to build a compact model with learned parameters (the weights $\mathbf{w}$ and bias $b$ in SVM or logistic regression, for example). Once trained, the model is just a small formula and the training data can be thrown away. Prediction = plug the new input into the formula.
  • Instance-based learning keeps the entire dataset as "the model." To predict a new input, it looks at the training examples most similar to it. The classic example is k-Nearest Neighbors (k-NN): to label a new point, find its $k$ nearest training points and take a majority vote of their labels.
Model-based Instance-based
What it stores a small set of parameters the whole dataset
Prediction cost cheap (one formula) costlier (compare to all data)
Boundary shape fixed by the model (e.g. a line) flexible / local

Analogy: model-based is a student who studies, writes a short cheat-sheet, then throws away the notes. Instance-based is a student who brings the whole textbook to the exam and looks up the closest similar problem.

In [8]:
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_moons

# A dataset that is NOT linearly separable: two interleaving moons
X, y = make_moons(n_samples=200, noise=0.20, random_state=5)

# Model-based: logistic regression (keeps only w, b at predict time)
log = LogisticRegression(max_iter=200)
log.fit(X, y)

# Instance-based: k-NN (keeps the whole dataset)
knn = KNeighborsClassifier(n_neighbors=5, n_jobs=1)
knn.fit(X, y)

# Build a grid so we can color the decision regions
x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 150),
                     np.linspace(y_min, y_max, 150))
grid = np.c_[xx.ravel(), yy.ravel()]

fig, axes = plt.subplots(1, 2, figsize=(11, 4))
for ax, model, title in [(axes[0], log, "Model-based: LogisticRegression"),
                         (axes[1], knn, "Instance-based: k-NN (k=5)")]:
    Z = model.predict(grid).reshape(xx.shape)
    ax.contourf(xx, yy, Z, alpha=0.3, cmap="coolwarm")
    ax.scatter(X[:, 0][y == 0], X[:, 1][y == 0], color="tab:blue",
               edgecolor="k", s=30, label="class 0")
    ax.scatter(X[:, 0][y == 1], X[:, 1][y == 1], color="tab:red",
               edgecolor="k", s=30, label="class 1")
    ax.set_title(title)
    ax.set_xlabel("feature 1")
    ax.set_ylabel("feature 2")
    ax.legend()
plt.tight_layout()
plt.show()
No description has been provided for this image

Reading the plots¶

Logistic regression (left) can only draw a straight boundary, so it misclassifies the curved moons. k-NN (right) needs no formula — it just votes among nearby points — so its boundary bends and wiggle to follow the moons. The trade-off: k-NN must carry the entire dataset to every prediction, while logistic regression only needs its handful of learned weights.

2.8 Shallow vs Deep Learning¶

  • Shallow learning learns its parameters directly from the input features. Logistic regression, SVMs, decision trees, k-NN — almost everything we've seen so far — are shallow. The model is essentially one step: features → parameters → output.
  • Deep learning stacks layers of tiny models (called neurons). Each layer transforms the previous layer's output into new, more abstract features. Most parameters are learned not from the raw features but from the outputs of the layers below. A network with more than one hidden layer between input and output is a deep neural network.

Analogy: shallow learning is a one-step recipe (mix ingredients → dish). Deep learning is an assembly line (raw parts → sub-assemblies → sub-sub-assemblies → final product), where each station learns to improve what the previous station handed it.

Don't worry if "layers" and "neurons" feel fuzzy now — we go hands-on with neural networks in Chapter 6. For now, let's just see the difference: a shallow model stuck with a straight boundary versus a deep model that can bend it.

In [9]:
from sklearn.linear_model import LogisticRegression
from sklearn.neural_network import MLPClassifier
from sklearn.datasets import make_moons

# Same two-moons dataset (curved, not linearly separable)
X, y = make_moons(n_samples=200, noise=0.20, random_state=5)

# Shallow: logistic regression -> straight-line boundary
shallow = LogisticRegression(max_iter=200)
shallow.fit(X, y)

# Deep: a small multi-layer perceptron (2 hidden layers) -> can bend the boundary
deep = MLPClassifier(hidden_layer_sizes=(10, 10), max_iter=500,
                     random_state=5)
deep.fit(X, y)

x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 150),
                     np.linspace(y_min, y_max, 150))
grid = np.c_[xx.ravel(), yy.ravel()]

fig, axes = plt.subplots(1, 2, figsize=(11, 4))
for ax, model, title in [(axes[0], shallow, "Shallow: LogisticRegression (linear)"),
                         (axes[1], deep, "Deep: MLPClassifier (2 hidden layers)")]:
    Z = model.predict(grid).reshape(xx.shape)
    ax.contourf(xx, yy, Z, alpha=0.3, cmap="coolwarm")
    ax.scatter(X[:, 0][y == 0], X[:, 1][y == 0], color="tab:blue",
               edgecolor="k", s=30, label="class 0")
    ax.scatter(X[:, 0][y == 1], X[:, 1][y == 1], color="tab:red",
               edgecolor="k", s=30, label="class 1")
    ax.set_title(title)
    ax.set_xlabel("feature 1")
    ax.set_ylabel("feature 2")
    ax.legend()
plt.tight_layout()
plt.show()

print(f"Shallow training accuracy: {shallow.score(X, y):.2f}")
print(f"Deep training accuracy   : {deep.score(X, y):.2f}")
C:\Users\DELL\anaconda3\Lib\site-packages\sklearn\neural_network\_multilayer_perceptron.py:691: ConvergenceWarning: Stochastic Optimizer: Maximum iterations (500) reached and the optimization hasn't converged yet.
  warnings.warn(
No description has been provided for this image
Shallow training accuracy: 0.88
Deep training accuracy   : 0.94

Reading the plots¶

The shallow logistic model is stuck with a straight boundary, so it slices through the moons and gets many points wrong. The deep MLP bends its boundary into a curve that hugs the two moons — and you can see the payoff in the accuracy numbers. That ability to learn features from previous layers is exactly what makes deep learning powerful for complex data like images and text.

Key Takeaways¶

  • A scalar is one number, a vector is an ordered list, a matrix is a grid, a set is unordered, and $\mathbb{R}$ is all real numbers; the feature vector $\mathbf{x}$ and label $y$ are supervised learning's basic objects.
  • The dot product combines two same-length vectors into one number; a norm measures length (2-norm = straight-line distance, 1-norm = taxicab distance).
  • A random variable has a PMF (discrete) or PDF (continuous); its mean $\mu$ and variance $\sigma^2$ summarize it, and the sample mean converges to the true mean as the dataset grows.
  • An unbiased estimator is right on average — the sample mean is unbiased, and sample variance needs the $N-1$ correction to be unbiased.
  • Bayes' rule flips conditionals (posterior $\propto$ likelihood × prior); a rare disease plus a decent test still yields a low posterior — the base-rate fallacy.
  • MLE maximizes data likelihood; MAP adds a prior and maximizes the posterior (MAP reduces to MLE under a flat prior, and the prior matters most when data is scarce).
  • Classification predicts a category, regression predicts a number; model-based learning compresses data into parameters while instance-based (k-NN) keeps the data; shallow maps features → output directly while deep stacks layers that build features from previous layers.

What's Next¶

In Chapter 3 — Fundamental Algorithms, we'll meet our first real supervised learning algorithms in detail — including linear regression and logistic regression — and watch them turn these definitions into working, trainable models.

Exercises¶

These exercises cover the notation and core ideas from Chapter 2 — vectors, norms, random variables, estimators, Bayes' rule, MLE/MAP, and the classification/regression and model/instance-based distinctions. Try each one before reading the hint.

  1. (Conceptual) Distinguish a scalar, vector, matrix, and set with one example each. In supervised learning, which two objects are $\mathbf{x}$ and $y$? Hint: $\mathbf{x}$ is the ordered list of inputs (the feature vector); $y$ is the label we predict.
  2. (Conceptual) The dot product of $\mathbf{w}=[1,2,3]$ and $\mathbf{x}=[4,5,6]$ is 32 — show the arithmetic. What does a dot product near 0 typically say about the two vectors' directions? Hint: near-zero dot product means they are roughly perpendicular.
  3. (Conceptual) For $\mathbf{v}=[3,4]$, the 2-norm is 5 and the 1-norm is 7. Using the bird/taxi analogy, explain why the 1-norm is never smaller than the 2-norm for the same vector. Hint: the straight-line path is the shortest possible route; any grid-only (horizontal+vertical) route is at least as long.
  4. (Conceptual) Contrast a PMF and a PDF. Why, for a continuous variable, is $\Pr(X=\text{exactly }5)=0$, and what quantity instead gives a nonzero probability? Hint: for continuous variables, probability comes from the area under the PDF over an interval, not the height at a single point.
  5. (Conceptual) Define "unbiased estimator" in one sentence. Why must the sample variance divide by $N-1$ (not $N$) to be unbiased, and what goes wrong if you divide by $N$? Hint: dividing by $N$ systematically underestimates the spread; the $N-1$ degrees-of-freedom correction removes that bias.
  6. (Conceptual) Restate Bayes' rule as posterior $\propto$ likelihood $\times$ prior. In the chapter's rare-disease example (1% prior, 99% sensitive, 95% specific) the posterior was about 16%. If the disease prevalence were 10% instead of 1%, would the posterior go up or down, and why? Hint: a higher prior means fewer of the positive tests are false positives from the large healthy majority.
  7. (Conceptual) How do MLE and MAP differ? Under what prior does MAP collapse to MLE, and in which situation (lots of data vs almost none) does the prior matter most? Hint: MAP = MLE plus a prior; with a flat prior or with tons of data the two agree, and the prior dominates when data is scarce.

Hands-On Coding Problems¶

  1. (Coding) Compute the dot product of w=[1,2,3] and x=[4,5,6] both with np.dot and by hand, plus the 2-norm and 1-norm of v=[3,4]. Hint: np.linalg.norm(v) defaults to the 2-norm; pass ord=1 for the 1-norm.
  2. (Coding) Demonstrate the Law of Large Numbers: draw 5000 samples from Normal(5, 2) and plot the running sample mean against the true mean. Hint: np.cumsum(sample) / np.arange(1, len(sample)+1) gives the running mean.
  3. (Coding) Show the sample mean is unbiased but variance-by-$N$ is biased: draw 8000 samples of size 10 from Normal(5, 2) and compare the average of s.var(ddof=0) vs s.var(ddof=1) to the true variance of 4. Hint: ddof=1 is the $N-1$ (unbiased) version.
  4. (Coding) Implement Bayes' rule for the diagnostic test: write a function that takes p_disease, sensitivity, and specificity and returns P(disease | positive). Call it for a 1% prior and again for a 10% prior. Hint: evidence P(+) = sensitivity*p_disease + (1-specificity)*(1-p_disease).
  5. (Coding) Visualize model-based vs instance-based learning: fit LogisticRegression and KNeighborsClassifier(n_neighbors=5) on make_moons(noise=0.20, random_state=5) and plot both decision regions on a meshgrid. Hint: use np.meshgrid + np.c_[xx.ravel(), yy.ravel()], model.predict, reshape, and contourf.
  6. (Coding) Visualize shallow vs deep: fit LogisticRegression and MLPClassifier(hidden_layer_sizes=(10,10), max_iter=500, random_state=5) on the same moons data, plot both decision regions, and print both training accuracies. Hint: the MLP should bend its boundary and score higher than the straight-line logistic model.
In [ ]:
# Exercise 8: dot product and norms
import numpy as np
w = np.array([1, 2, 3])
x = np.array([4, 5, 6])
v = np.array([3, 4])

# TODO: dot product with np.dot AND by hand (sum of products)
dot = 0  # placeholder

# TODO: 2-norm and 1-norm of v
l2 = 0  # placeholder
l1 = 0  # placeholder

print("dot =", dot, " (expected 32)")
print("||v||_2 =", l2, " (expected 5)")
print("||v||_1 =", l1, " (expected 7)")
In [ ]:
# Exercise 9: Law of Large Numbers -- running sample mean
import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(42)
sample = rng.normal(loc=5.0, scale=2.0, size=5000)

# TODO: compute the running sample mean
n_vals = np.arange(1, len(sample) + 1)
running_mean = np.full(len(sample), 5.0)  # placeholder

# TODO: plot running_mean vs n_vals with a horizontal line at the true mean 5.0
plt.show()
In [ ]:
# Exercise 10: sample mean is unbiased, variance-by-N is biased
import numpy as np
rng = np.random.default_rng(7)
true_mean, true_std, true_var = 5.0, 2.0, 4.0
N, repeats = 10, 8000
sample_means = np.empty(repeats)
biased_vars = np.empty(repeats)
unbiased_vars = np.empty(repeats)

for r in range(repeats):
    s = rng.normal(loc=true_mean, scale=true_std, size=N)
    sample_means[r] = s.mean()
    # TODO: biased_vars[r] = s.var(ddof=0)  and  unbiased_vars[r] = s.var(ddof=1)
    biased_vars[r] = 0      # placeholder
    unbiased_vars[r] = 0    # placeholder

print("true variance =", true_var)
print("avg biased variance (ddof=0)   =", biased_vars.mean())
print("avg unbiased variance (ddof=1) =", unbiased_vars.mean())
In [ ]:
# Exercise 11: Bayes' rule for a diagnostic test
def posterior_disease_given_positive(p_disease, sensitivity, specificity):
    # TODO: compute P(disease | +) using Bayes' rule
    # evidence P(+) = sensitivity*p_disease + (1-specificity)*(1-p_disease)
    return 0.0  # placeholder

sensitivity, specificity = 0.99, 0.95
print("posterior (1% prior):", posterior_disease_given_positive(0.01, sensitivity, specificity))

# TODO: call it again with p_disease = 0.10 and print the result
In [ ]:
# Exercise 12: model-based vs instance-based decision regions
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.datasets import make_moons

X, y = make_moons(n_samples=200, noise=0.20, random_state=5)

# TODO: fit LogisticRegression (model-based) and KNeighborsClassifier(n_neighbors=5)
log = None   # placeholder
knn = None   # placeholder

# TODO: build a meshgrid over the data range and plot each model's decision regions
# Hint: np.meshgrid + np.c_[xx.ravel(), yy.ravel()] -> predict -> reshape -> contourf
plt.show()
In [ ]:
# Exercise 13: shallow vs deep on the moons
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
from sklearn.neural_network import MLPClassifier
from sklearn.datasets import make_moons

X, y = make_moons(n_samples=200, noise=0.20, random_state=5)

# TODO: fit a shallow LogisticRegression and a deep MLPClassifier(hidden_layer_sizes=(10,10), max_iter=500, random_state=5)
shallow = None  # placeholder
deep = None     # placeholder

# TODO: plot both decision regions and print both training accuracies (.score(X, y))
plt.show()