Last Updated: July 18, 2026

Probabilistic Machine Learning: An Introduction by Kevin P. Murphy

Note: This post is a structured, code-backed study companion summarizing the foundational chapters of “Probabilistic Machine Learning: An Introduction” (2022) by Kevin P. Murphy. The core conceptual definitions, notations, and structural progressions are adapted directly from his text.

To provide deeper conceptual clarity, paragraphs styled in italics throughout this post have been generated and expanded with the assistance of AI to break down the underlying mathematical explanations and intuition.

Contact

What is Machine Learning

The definition of ML, according to Tom Mitchell, is as follows:

A computer program is said to learn from experience E with respect to some class of tasks T, and performance measure P, if its performance at tasks in T, as measured by P, improves with experience E.

Supervised Learning

The task TT is to learn a mapping ff from inputs xXx \in X to outputs yYy \in Y. The inputs xx are also called the features, covariates, or predictor. The output yy is also known as the label, target, or response.

The experience EE is given in the form of a set of NN input-output pairs 𝒟={(xn,yn)}n=1N\mathcal{D} = \{(x_n, y_n)\}_{n=1}^N, known as the training set (where NN is called the sample size). The performance measure P depends on the type of output we are predicting.

Classification

The output space is a set of CC unordered and mutually exclusive labels known as classes, 𝒴={1,2,,C}\mathcal{Y} = \{1, 2, \dots, C\}.

The problem of predicting the class label given an input is also called pattern recognition. If there are just two classes, often denoted by y{0,1}ory{1,+1}y \in \{0, 1\} or y \in \{-1, +1\}, it is called binary classification.

When we have small datasets of features, it is common to store them in an N×DN \times D matrix, in which each row represents an example, and each column represents a feature. This is known as a design matrix.

Exploratory Data Analysis

Before tackling a problem with ML, it is usually a good idea to perform exploratory data analysis, to see if there are any obvious patterns (which might give hints on what method to choose), or any obvious problems with the data (e.g., label noise or outliers).

For tabular data with a small number of features, it is common to make a pair plot. For higher-dimensional data, it is common to first perform dimensionality reduction, and then to visualize the data in 2d or 3d.

Learning a Classifier

Classifiers divide data into different regions using a decision boundary. A decision boundary is the “line in the sand” that a classifier draws to separate different classes of data. Decision Trees are built by repeatedly splitting these regions with “if/then” rules to fix classification errors.

Tree Parameters (θ\theta) are simply the specific features and cutoff values (thresholds) used to make those splits.

Empirical Risk Minimization

Empirical Risk Minimization (ERM): A training method where the goal is to find parameters θ\theta that minimize the average loss on the training set.

Loss Function (\ell): Measures the cost of a prediction error.

Empirical Risk (L(θ)L(\theta)): The average loss of the model across all NN training examples.

(θ)1Nn=1N(yn,f(xn;θ))\mathcal{L}(\theta) \triangleq \frac{1}{N} \sum_{n=1}^N \ell(y_n, f(x_n;\theta))

While ERM minimizes training loss, the true objective is generalization (minimizing expected loss on unseen future data).

Uncertainty

In many cases, we will not be able to perfectly predict the exact output given the input, due to lack of knowledge of the input-output mapping (this is called epistemic uncertainty or model uncertainty), and/or due to intrinsic (irreducible) stochasticity in the mapping (this is called aleatoric uncertainty or data uncertainty).

Conditional Probability: To capture model uncertainty, we predict a probability distribution over the CC possible classes: p(y=c|x;θ)=fc(x;θ)p(y = c \mid x; \theta) = f_c(x; \theta)​.

Logits and Softmax: Instead of forcing the model f(x;θ)f(x; \theta) to output valid probabilities directly (which must be between 0 and 1, and sum to 1), we let it output raw, unnormalized log-probabilities called logits (aa). We then pass these through the softmax function to convert them into a valid probability distribution: p(y=c|x;θ)=softmaxc(f(x;θ))p(y = c \mid x; \theta) = \operatorname{softmax}_c(f(x; \theta)).

import tensorflow as tf

def softmax_tf(logits_input):
    return tf.nn.softmax(tf.constant(logits_input, dtype=tf.float32))

# N = 2 samples, C = 3 possible events/classes (Raw, unnormalized scores)
logits = [
    [1.0, 2.0, 3.0],  # Sample 1 raw scores
    [2.0, 4.0, 2.0]   # Sample 2 raw scores
]

probabilities = softmax_tf(logits)

print("TF Softmax Probabilities:\n", probabilities.numpy())
# Output: 
# TF Softmax Probabilities:
# [[0.09003057 0.24472848 0.66524094]
# [0.10650697 0.786986   0.10650697]]
import torch

def softmax_torch(logits_input):
    logits_tensor = torch.tensor(logits_input, dtype=torch.float32)
    # dim=-1 explicitly specifies softmax over the final class columns axis
    return torch.nn.functional.softmax(logits_tensor, dim=-1)

# N = 2 samples, C = 3 possible events/classes (Raw, unnormalized scores)
logits = [
    [1.0, 2.0, 3.0],
    [2.0, 4.0, 2.0]
]

probabilities = softmax_torch(logits)

print("PyTorch Softmax Probabilities:\n", probabilities.numpy())
# Output: 
# PyTorch Softmax Probabilities:
# [[0.09003057 0.24472848 0.66524094]
# [0.10650697 0.786986   0.10650697]]
import numpy as np

def softmax_numpy(logits_input):
    logits_arr = np.asarray(logits_input)
    
    # Subtracting the max value per row prevents system numerical overflow errors
    stable_logits = logits_arr - np.max(logits_arr, axis=-1, keepdims=True)
    
    exponentiated = np.exp(stable_logits)
    return exponentiated / np.sum(exponentiated, axis=-1, keepdims=True)

# N = 2 samples, C = 3 possible events/classes (Raw, unnormalized scores)
logits = [
    [1.0, 2.0, 3.0],
    [2.0, 4.0, 2.0]
]

probabilities = softmax_numpy(logits)

print("NP Softmax Probabilities:\n", probabilities)
# Output: 
# NP Softmax Probabilities:
# [[0.09003057 0.24472847 0.66524096]
# [0.10650698 0.78698604 0.10650698]]
p(y = c \mid x; \theta) = \operatorname{softmax}_c(f(x; \theta))

Logistic Regression: A special case where ff is an affine function. In Machine Learning, the parameters θ=(b,w)\theta = (b, w) are called the bias (bb) and the weights w:f(x;θ)=b+wTxw: f(x; \theta) = b + w^T x​.

Notation Simplification: To simplify formulas, we can absorb the bias bb into the weights vector ww by prepending a 1 to our input vector xx (making it x~=[1,x1,,XD]T\tilde{x} = [1, x_1, \dots, X_D]^T. This turns the affine function into a simple linear dot product: f(x;w)=wTxf(x; w) = w^T x.

Maximum Likelihood Estimation

Negative Log Probability (Loss Function): When using probabilistic models, the loss function is typically defined as the negative log of the predicted probability of the true label: (y,f(x;θ))=logp(y|f(x;θ))\ell(y, f(x; \theta)) = -\log p(y \mid f(x; \theta)).

Negative Log Likelihood (NLL): The empirical risk (average loss) of this model over the entire training set of NN examples:

NLL(θ)=1Nn=1Nlogp(yn|f(xn;θ))\text{NLL}(\theta) = -\frac{1}{N} \sum_{n=1}^N \log p(y_n \mid f(x_n; \theta))
import tensorflow as tf

def nll_logits_tf(y_true, logits):
    loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(
        from_logits=True,
        reduction=tf.keras.losses.Reduction.NONE
    )

    return loss_fn(
        tf.convert_to_tensor(y_true),
        tf.convert_to_tensor(logits, dtype=tf.float32)
    )


# N = 2 samples, C = 3 classes
y_true = [1, 2]

# Raw logits (not probabilities)
logits = [
    [1.0, 2.5, 1.2],  # predicts class 1
    [0.8, 0.5, 2.0]   # predicts class 2
]

loss_per_sample = nll_logits_tf(y_true, logits)
total_loss = tf.reduce_mean(loss_per_sample)

print("TF NLL per sample:", loss_per_sample.numpy())
print("TF NLL (Final Average):", total_loss.numpy())
# Output: 
# TF NLL per sample: [0.4025689  0.42155132]
# TF NLL (Final Average): 0.4120601
import torch

def nll_logits_torch(y_true, logits):
    y_true_tensor = torch.tensor(y_true, dtype=torch.long)
    logits_tensor = torch.tensor(logits, dtype=torch.float32)

    loss_individual = torch.nn.functional.cross_entropy(
        logits_tensor,
        y_true_tensor,
        reduction="none"
    )

    return loss_individual


# N = 2 samples, C = 3 classes
y_true = [1, 2]

# Raw logits
logits = [
    [1.0, 2.5, 1.2],
    [0.8, 0.5, 2.0]
]

loss_per_sample = nll_logits_torch(y_true, logits)
total_loss = torch.mean(loss_per_sample)

print("PyTorch NLL per sample:", loss_per_sample.numpy())
print("PyTorch NLL (Final Average):", total_loss.item())
# Output: 
# PyTorch NLL per sample: [0.4025689  0.42155132]
# PyTorch NLL (Final Average): 0.41206011176109314
import numpy as np

def nll_logits_numpy(y_true, logits):
    y_true_arr = np.asarray(y_true)
    logits_arr = np.asarray(logits)

    # Stable log-softmax
    logits_shifted = logits_arr - np.max(
        logits_arr,
        axis=-1,
        keepdims=True
    )

    log_probs = (
        logits_shifted
        - np.log(np.sum(np.exp(logits_shifted), axis=-1, keepdims=True))
    )

    # Pick log probability of true class
    loss_individual = -log_probs[
        np.arange(len(y_true_arr)),
        y_true_arr
    ]

    return loss_individual


y_true = [1, 2]

logits = [
    [1.0, 2.5, 1.2],
    [0.8, 0.5, 2.0]
]

loss_per_sample = nll_logits_numpy(y_true, logits)
total_loss = np.mean(loss_per_sample)

print("NP NLL per sample:", loss_per_sample)
print("NP NLL (Final Average):", total_loss)
# Output: 
# NP NLL per sample: [0.40256889 0.42155128]
# NP NLL (Final Average): 0.41206008203499644
\text{NLL}(\theta) = -\frac{1}{N} \sum_{n=1}^N \log p(y_n \mid f(x_n; \theta))

When the data contains only two classes (y{0,1}y \in \{0,1\}):

=1Nn=1N[ynlog(pn)+(1yn)log(1pn)]\mathcal{L}=-\frac{1}{N}\sum_{n=1}^N\left[ y_n \log(p_n) + (1-y_n)\log(1-p_n)\right]

Because yny_n must be either 0 or 1, one half of the equation cancels out completely for every sample, isolating the log probability of the correct target.

import tensorflow as tf

def bce_logits_tf(y_true, logits_pred):
    y_true_tensor = tf.convert_to_tensor(y_true, dtype=tf.float32)
    logits_tensor = tf.convert_to_tensor(logits_pred, dtype=tf.float32)

    bce_loss = tf.keras.losses.BinaryCrossentropy(
        from_logits=True,
        reduction=tf.keras.losses.Reduction.NONE
    )

    return bce_loss(y_true_tensor, logits_tensor)


# N = 2 separate samples
y_true = [[1.0], [1.0]]

# Raw logits (not probabilities)
logits_pred = [[1.386], [2.197]]

loss_per_sample = bce_logits_tf(y_true, logits_pred)
total_loss = tf.reduce_mean(loss_per_sample)

print("TF BCE (Logits) per sample:", loss_per_sample.numpy())
print("TF BCE (Logits) (Final Average):", total_loss.numpy())
# Output: 
# TF BCE (Logits) per sample: [0.2232024  0.10538297]
# TF BCE (Logits) (Final Average): 0.1642927
import torch

def bce_logits_torch(y_true, logits_pred):
    y_true_tensor = torch.tensor(y_true, dtype=torch.float32)
    logits_tensor = torch.tensor(logits_pred, dtype=torch.float32)

    bce_loss = torch.nn.BCEWithLogitsLoss(
        reduction="none"
    )

    return bce_loss(logits_tensor, y_true_tensor)


# N = 2 separate samples
y_true = [[1.0], [1.0]]

# Raw logits (not probabilities)
logits_pred = [[1.386], [2.197]]

loss_per_sample = bce_logits_torch(y_true, logits_pred)
total_loss = torch.mean(loss_per_sample)

print("PyTorch BCE (Logits) per sample:", loss_per_sample.numpy())
print("PyTorch BCE (Logits) (Final Average):", total_loss.numpy())

# Output: 
# PyTorch BCE (Logits) per sample: [[0.22320242]
# [0.10538297]]
# PyTorch BCE (Logits) (Final Average): 0.1642927
import numpy as np

def bce_logits_numpy(y_true, logits_pred):
    y_true_arr = np.asarray(y_true, dtype=np.float32)
    logits_arr = np.asarray(logits_pred, dtype=np.float32)

    bce_loss = np.maximum(logits_arr, 0) - (y_true_arr * logits_arr) + np.log1p(
        np.exp(-np.abs(logits_arr))
    )

    return bce_loss


# N = 2 separate samples
y_true = [[1.0], [1.0]]

# Raw logits (not probabilities)
logits_pred = [[1.386], [2.197]]

loss_per_sample = bce_logits_numpy(y_true, logits_pred)
total_loss = np.mean(loss_per_sample)

print("NP BCE (Logits) per sample:", loss_per_sample.squeeze())
print("NP BCE (Logits) (Final Average):", total_loss)

# Output: 
# NP BCE (Logits) per sample: [0.2232024  0.10538296]
# NP BCE (Logits) (Final Average): 0.1642927
\mathcal{L}=-\frac{1}{N}\sum_{n=1}^N\left[ y_n \log(p_n) + (1-y_n)\log(1-p_n)\right]

Maximum Likelihood Estimate (MLE): The specific parameter settings θ^mle\hat{\theta}_{\text{mle}} found by minimizing the NLL: θ^mle=argminθNLL(θ)\hat{\theta}_{\text{mle}} = \arg\min\limits_{\theta} \text{NLL}(\theta).

Regression

Now suppose that we want to predict a real-valued quantity yy \in \mathbb{R} instead of a class label y{1,,C}y \in \{1, \dots, C\}; this is known as regression.

For regression, the most common choice is to use quadratic loss, or 2\ell_2 loss: 2(y,y^)=(yy^)2\ell_2(y, \hat{y}) = (y – \hat{y})^2.

The empirical risk when using quadratic loss is equal to the mean squared error or MSE:

MSE(θ)=1Nn=1N(ynf(xn;θ))2\text{MSE}(\theta) = \frac{1}{N} \sum_{n=1}^N (y_n – f(x_n; \theta))^2
import tensorflow as tf

def empirical_risk_tf(y_true, y_pred):
    return tf.keras.losses.MSE(y_true, y_pred)

print("TF MSE: ", empirical_risk_tf([2, 1], [4, 5]))
# Output: TF MSE:  tf.Tensor(10.0, shape=(), dtype=float32)
import torch

def empirical_risk_torch(y_true, y_pred):
    return torch.nn.functional.mse_loss(y_true, y_pred)

y_true = torch.tensor([2.0, 1.0])
y_pred = torch.tensor([4.0, 5.0])

print("PyTorch MSE: ", empirical_risk_torch(y_true, y_pred).item())
# Output: PyTorch MSE:  10.0
import numpy as np

def empirical_risk_numpy(y_true, y_pred):
    return np.mean(np.square(np.subtract(y_true, y_pred)))

print("NP MSE:", empirical_risk_numpy([2, 1], [4, 5])) 
# Output: NP MSE: 10.0
\text{MSE}(\theta) = \frac{1}{N} \sum_{n=1}^N (y_n - f(x_n; \theta))^2

In regression problems, it is common to assume the output distribution is a Gaussian or normal:

𝒩(y|μ,σ2)12πσ2exp(12σ2(yμ)2)\mathcal{N}(y \mid \mu, \sigma^2) \triangleq \frac{1}{\sqrt{2\pi\sigma^2}} \exp\left({-\frac{1}{2\sigma^2} (y – \mu)^2}\right)

In the context of regression, we can make the mean depend on the inputs by defining μ=f(xn;θ)\mu = f(x_n; θ). We therefore get the following conditional probability distribution:

p(yn|xn;θ)=𝒩(yn|f(xn;θ),σ2)p(y_n|x_n;\theta) = \mathcal{N}(y_n|f(x_n;\theta), \sigma^2)

If we assume that the variance σ2\sigma^2 is fixed (for simplicity), the corresponding average (per-sample) negative log likelihood becomes:

NLL(θ)=1Nn=1Nlog[(12πσ2)12exp(12σ2(ynf(xn;θ))2))]=12σ2MSE(θ)+const\text{NLL}(\theta) = -\frac{1}{N} \sum_{n=1}^N {\log \left[({\frac{1}{2\pi{\sigma^2}}})^{\frac{1}{2}} \exp \left(-{\frac{1}{2{\sigma^2}}} (y_n – f(x_n; \theta)) ^2)\right)\right]}\\ = \frac{1}{2{\sigma^2}}\text{MSE}(\theta) + \text{const}

We see that the NLL\text{NLL} is proportional to the MSE\text{MSE}. Hence computing the maximum likelihood estimate of the parameters will result in minimizing the squared error, which seems like a sensible approach to model fitting.

Linear Regression

We can fit a 1D data using a simple linear regression model of the form: f(x;θ)=b+wxf(x;\theta) = b + wx, where ww is the slope, bb is the offset, and θ=(w,b)\theta = (w,b) are all the parameters of the model.

If we have multiple input features, we can write: f(x;θ)=b+w1x1++wDxD=b+wTxf(x;\theta) = b + w_1x_1 + \dots + w_Dx_D = b + w^Tx. This is called multiple linear regression.

Polynomial Regression

We can improve the fit by using a polynomial regression model of degree DD. This has the form f(x;w)=wTϕ(x)f(x; w) = w^T\phi(x), where ϕ(x)\phi(x) is a feature vector derived from the input, which has the following form: ϕ(x)=[1,x,x2,,xD]\phi(x) = \left[ 1, x, x^2, \dots, x^D \right].

import sklearn
import numpy as np

poly = sklearn.preprocessing.PolynomialFeatures(degree=3, include_bias=True)

# Scikit-learn expects a 2D grid. Here, we have a batch of 3 samples, each with 1 feature.
x = np.array([[1.0], 
              [2.0], 
              [3.0]])

phi_x = poly.fit_transform(x)

print("Scikit-learn Polynomial Features Matrix:\n", phi_x)
# Output: Scikit-learn Polynomial Features Matrix:
#  [[ 1.  1.  1.  1.]
#  [ 1.  2.  4.  8.]
#  [ 1.  3.  9. 27.]]
# Meaning:
# phi(1) => phi_x[0] = [ 1.  1.  1.  1.]
# phi(2) => phi_x[1] = [ 1.  2.  4.  8.]
# phi(3) => phi_x[2] = [ 1.  3.  9. 27.]
\phi(x) = \left[ 1, x, x^2, \dots, x^D \right]

i.e. Linear: f^(x)=w0+w1x1+w2x2\hat{f}(x) = w_0 + w_1x_1 + w_2x_2; Polynomial: f^(x)=w0+w1x1+w2x2+w3x12+w4x22\hat{f}(x) = w_0 + w_1x_1 + w_2x_2 + w_3x_1^2 + w_4x_2^2.

This is a simple example of feature preprocessing, also called feature engineering.

Note that the above models still use a prediction function that is a linear function of the parameters ww, even though it is a nonlinear function of the original input xx.

Deep Neural Networks

We had manually specified the transformation of the input features, namely polynomial expansion, ϕ(x)=[1,x1,x2,x12,x22,]\phi(x) = \left[ 1, x_1, x_2, x_1^2, x_2^2, \dots\right]. We can create much more powerful models by learning to do such nonlinear feature extraction automatically. If we let ϕ(x)\phi(x) have its own set of parameters, say 𝐕\mathbf{V}, then the overall model has the form: f(x;w,V)=wTϕ(x;V)f(x; w, V) = w^T\phi(x;V).

We can recursively decompose the feature extractor ϕ(x;V)\phi(x; V) into a composition of simpler functions. The resulting model then becomes a stack of LL nested functions:

f(x;θ)=fL(fL1((f1(x))))f(x;\theta) = f_L(f_{L-1}(\dots(f_1(x))\dots))

where f(x)=f(x;θ)f_{\ell}(x) = f(x; \theta_{\ell}) is the function at layer \ell.

Overfitting and Generalization

We can rewrite the empirical risk:

(θ)1Nn=1N(yn,f(xn;θ))\mathcal{L}(\theta) \triangleq \frac{1}{N} \sum_{n=1}^N \ell(y_n, f(x_n;\theta))

in the following equivalent way:

(θ;Dtrain)1|Dtrain|(x,y)DtrainN(y,f(x;θ))\mathcal{L}(\theta; D_{train}) \triangleq \frac{1}{\left|D_{train}\right|} \sum_{(x, y) \in D_{train}}^N \ell(y, f(x;\theta))

where |Dtrain||D_{train}| is the size of the training set DtrainD_{train}. This formulation is useful because it makes explicit which dataset the loss is being evaluated on.

A model that perfectly fits the training data, but which is too complex, is said to suffer from overfitting.

To detect if a model is overfitting, let us assume (for now) that we have access to the true (but unknown) distribution p(x,y)p^\ast(x,y) used to generate the training set. Then, instead of computing the empirical risk we compute the theoretical expected loss or population risk:

(θ;p)𝔼p(x,y)[(y,f(x;θ))]\mathcal{L}(\theta; p^\ast) \triangleq \mathbb{E}_{p^\ast(x, y)} \left[ \ell(y, f(x;\theta)) \right]

The difference (θ;p)(θ;Dtrain)\mathcal{L}(\theta; p^\ast) – \mathcal{L}(\theta; D_{train}) is called the generalization gap. If a model has a large generalization gap (i.e., low empirical risk but high population risk), it is a sign that it is overfitting.

In practice we don’t know pp^*. However, we can partition the data we do have into two subsets, known as the training set and the test set. Then we can approximate the population risk using the test risk:

(θ;Dtest)1|Dtest|(x,y)DtestN(y,f(x;θ))\mathcal{L}(\theta; D_{test}) \triangleq \frac{1}{\left|D_{test}\right|} \sum_{(x, y) \in D_{test}}^N \ell(y, f(x;\theta))

In practice, we need to partition the data into three sets, namely the training set, the test set and a validation set; the latter is used for model selection, and we just use the test set to estimate future performance (the population risk), i.e., the test set is not used for model fitting or model selection.

No Free Lunch Theorem

Given the large variety of models in the literature, it is natural to wonder which one is best. Unfortunately, there is no single best model that works optimally for all kinds of problems. The best way to pick a suitable model is based on domain knowledge, and/or trial and error. For this reason, it is important to have many models and algorithmic techniques in one’s toolbox to choose from.

Unsupervised Learning

An arguably much more interesting task is to try to “make sense of” data, as opposed to just learning a mapping. That is, we just get observed “inputs” D={xn:n=1:N}D = \left\{ x_n: n=1: N \right\} without any corresponding “outputs” yny_n. This is called unsupervised learning.

From a probabilistic perspective, we can view the task of unsupervised learning as fitting an unconditional model of the form p(x)p(x), which can generate new data xx, whereas supervised learning involves fitting a conditional model, p(y|x)p(y|x), which specifies (a distribution over) outputs given inputs.

Clustering

A simple example of unsupervised learning is the problem of finding clusters in data. The goal is to partition the input into regions that contain “similar” points.

Discovering Latent “Factors of Variation”

In the real world, data often has too many variables (high-dimensional, xnDx_n \in \mathbb{R}^D). However, we assume that this complex, observed data is actually driven by a much smaller set of hidden, underlying concepts called latent variables (znKz_n \in \mathbb{R}^K).

Imagine you have a high-definition video (xx) of a person’s 3D face consisting of millions of pixels (DD). In reality, those millions of pixels are completely controlled by just a few facial muscles (zz, size KK). If you can find zz, you capture the “essence” of the video in a fraction of the size.

The Generative Direction (znxnz_n \to x_n): We model the world as a generative process. First, nature chooses some latent factors znz_n (e.g., facial muscle positions) from a simple prior distribution like a Gaussian p(zn)p(z_n). Then, those factors generate our observed data xnx_n (the pixels) through some likelihood function p(xn|zn)p(x_n \mid z_n).

The simplest example is when we use a linear model, p(xn|zn;θ)=𝒩(xn|Wzn+μ,Σ)p(x_n \mid z_n; \theta) = \mathcal{N}(x_n \mid Wz_n + \mu, \Sigma).

Here, we say that the observed data xnx_n is a linear transformation (Wzn+μ)(Wz_n + \mu) of our hidden factors, plus some added Gaussian noise (represented by the covariance matrix Σ\Sigma).

You have to figure out both the inputs zz and the weights WW at the same time.

In Factor Analysis, the noise Σ\Sigma can be different for each observed dimension (a diagonal matrix with different values on the diagonal). It means some pixel sensors might be noisier than others.

In Probabilistic PCA, we assume the noise is exactly the same in every direction (Σ=σ2I\Sigma = \sigma^2 I, where II is the identity matrix). This makes the math cleaner and directly aligns with classic geometric Principal Component Analysis (PCA).

While linear models are elegant, the real world is rarely linear. To fix this, we can replace the linear mapping WznWz_n with a nonlinear function, f(zn;θf(z_n; \theta):

p(xn|zn;θ)=𝒩(xn|f(zn;θ),σ2I)p(x_n \mid z_n; \theta) = \mathcal{N}(x_n \mid f(z_n; \theta), \sigma^2 I)

f(zn;θ)f(z_n; \theta) is a deep neural network. This network acts as a decoder. It takes a tiny latent vector zz and learns a highly complex, nonlinear way to reconstruct the high-dimensional data xx. Estimating the parameters (θ\theta) of the neural network is incredibly hard when you don’t even know what the inputs (zz) are. This is where the Variational Autoencoder (VAE) comes in. It solves this optimization nightmare by training a second neural network (the encoder, or inference network) that learns to guess the latent variables zz directly from the inputs xx.

Self-supervised Learning

In this approach, we create proxy supervised tasks from unlabeled data.

It uses the exact same math, loss functions, and optimization algorithms as supervised learning, but the training targets are automatically extracted from the raw data itself.

We do not actually care about the model’s performance on the proxy task (e.g., predicting missing words). We only care about the high-quality, reusable feature representations the model is forced to develop in order to solve that task.

Evaluating Unsupervised Learning

Evaluating unsupervised learning is incredibly hard because there are no correct labels (“ground truth”) to compare the results against. To solve this, researchers use three main approaches to measure success:

Reinforcement Learning

The system or agent has to learn how to interact with its environment. This can be encoded by means of a policy a=π(x)a = \pi(x), which specifies which action to take in response to each possible input xx (derived from the environment state).

The difference from supervised learning (SL) is that the system is not told which action is the best one to take (i.e., which output to produce for a given input). Instead, the system just receives an occasional reward (or punishment) signal in response to the actions that it takes.

To compensate for the minimal amount of information coming from the reward signal, it is common to use other information sources, such as expert demonstrations, which can be used in a supervised way, or unlabeled data, which can be used by an unsupervised learning system to discover the underlying structure of the environment.

Data

The nature and quality of the training data also plays a vital role in the success of any learned model.

Some Common Image Datasets

Small Image Datasets

ImageNet

This is a dataset of ∼14M images of size 256 ×256 ×3 illustrating various objects from 20,000 classes; was used as the basis of the ImageNet Large Scale Visual Recognition Challenge (ILSVRC), which ran from 2010 to 2018.

Some Common Text Datasets

Machine learning is often applied to text to solve a variety of tasks. This is known as natural language processing or NLP.

Text Classification

IMDB (Internet Movie Database): contains 25k labeled examples for training, and 25k for testing. Each example has a binary label, representing a positive or negative rating.

Machine Translation

Other seq2seq Tasks

A generalization of machine translation is to learn a mapping from one sequence xx to any other sequence yy. This is called a seq2seq model, and can be viewed as a form of high-dimensional classification. Includes tasks:

Language Modeling

Refers to the task of creating unconditional generative models of text sequences, p(x1,...,xT)p(x_1,…,x_T). This only requires input sentences xx, without any corresponding “labels” yy. We can therefore think of this as a form of unsupervised learning. If the language model generates output in response to an input, as in seq2seq, we can regard it as a conditional generative model.

Preprocessing Discrete Input Data

One-hot Encoding

When we have categorical features, we need to convert them to a numerical scale, so that computing weighted combinations of the inputs makes sense.

If a variable xx has KK values, we will denote its dummy encoding as follows: one-hot(x)=[𝕀(x=1),,𝕀(x=K)]\text{one-hot}(x) = \left[ \mathbb{I}(x=1), \dots, \mathbb{I}(x=K) \right]. For example, if there are 3 colors (say red, green and blue), the corresponding one-hot vectors will be one-hot(red)=[1,0,0],one-hot(green)=[0,1,0],one-hot(blue)=[0,0,1]\text{one-hot}(red) = [1,0,0], \text{one-hot}(green) = [0, 1,0], \text{one-hot}(blue) = [0,0,1].

Feature Crosses

A linear model using a dummy encoding for each categorical variable can capture the main effects of each variable, but cannot capture interaction effects between them.

For example, suppose we want to predict the fuel efficiency of a vehicle given two categorical input variables: the type (say SUV, Truck, or Family car), and the country of origin (say USA or Japan). If we concatenate the one-hot encodings for the ternary and binary features, we get the following input encoding:

ϕ(x)=[1,𝕀(x1=S),𝕀(x1=T),𝕀(x1=F),𝕀(x2=U),𝕀(x2=J)]\phi(x) = \left[ 1, \mathbb{I}(x_1=S), \mathbb{I}(x_1=T), \mathbb{I}(x_1=F), \mathbb{I}(x_2=U), \mathbb{I}(x_2=J) \right]

where x1x_1 is the type and x2x_2 is the country of origin.

We can fix this by computing explicit feature crosses:

f(x;w)=wTϕ(x)=w0+w1𝕀(x1=S)+w2𝕀(x1=T)+w3𝕀(x1=F)+w4𝕀(x2=U)+w5𝕀(x2=J)+w6𝕀(x1=S,x2=U)+w7𝕀(x1=T,x2=U)+w8𝕀(x1=F,x2=U)+w9𝕀(x1=S,x2=J)+w10𝕀(x1=T,x2=J)+w11𝕀(x1=F,x2=J)f(x; w) = w^T\phi(x) \\ = w_0 + w_1\mathbb{I}(x_1=S) + w_2\mathbb{I}(x_1=T) + w_3\mathbb{I}(x_1=F) + \\ w_4\mathbb{I}(x_2=U) + w_5\mathbb{I}(x_2=J) + w_6\mathbb{I}(x_1=S,x_2=U) + \\ w_7\mathbb{I}(x_1=T,x_2=U) + w_8\mathbb{I}(x_1=F,x_2=U) + \\ w_9\mathbb{I}(x_1=S,x_2=J) + w_{10}\mathbb{I}(x_1=T,x_2=J) + \\ w_{11}\mathbb{I}(x_1=F,x_2=J)

We can see that the use of feature crosses converts the original dataset into a wide format, with many more columns:

ϕ(x)=[1,𝕀(x1=S),𝕀(x1=T),𝕀(x1=F),𝕀(x2=U),𝕀(x2=J),𝕀(x1=S,x2=U),𝕀(x1=T,x2=U),𝕀(x1=F,x2=U),𝕀(x1=S,x2=J),𝕀(x1=T,x2=J),𝕀(x1=F,x2=J)]\phi(x) = \left[ \begin{aligned} 1,\mathbb{I}(x_1=S), \mathbb{I}(x_1=T),\mathbb{I}(x_1=F), \mathbb{I}(x_2=U),\mathbb{I}(x_2=J), \\ \mathbb{I}(x_1=S,x_2=U) ,\mathbb{I}(x_1=T,x_2=U),\mathbb{I}(x_1=F,x_2=U),\\ \mathbb{I}(x_1=S,x_2=J),\mathbb{I}(x_1=T,x_2=J), \mathbb{I}(x_1=F,x_2=J) \end{aligned} \right]

import pandas as pd

df = pd.DataFrame({
    'col1': [1, 2, 3, 1], # S, T, F, S
    'col2': [1, 2, 1, 2]  # U, J, U, J
})

combined_series = df['col1'].astype(str) + "_" + df['col2'].astype(str)

df['combined_col'] = combined_series.astype('category').cat.codes + 1

print("Pandas DataFrame:\n ",df)
# Output: Pandas DataFrame:
#    col1  col2  combined_col
# 0     1     1             1
# 1     2     2             3
# 2     3     1             4
# 3     1     2             2

df['col1'] = pd.Categorical(df['col1'], categories=[1, 2, 3])
df['col2'] = pd.Categorical(df['col2'], categories=[1, 2])
df['combined_col'] = pd.Categorical(df['combined_col'], categories=[1, 2, 3, 4, 5, 6])

one_hots_df = pd.get_dummies(df, columns=['col1', 'col2', 'combined_col'], dtype=float)

one_hots_df.insert(0, 'bias', 1.0)

phi_x = one_hots_df.to_numpy()
print("phi:\n", phi_x)
# Output: 
# phi:
# [[1. 1. 0. 0. 1. 0. 1. 0. 0. 0. 0. 0.]
#  [1. 0. 1. 0. 0. 1. 0. 0. 1. 0. 0. 0.]
#  [1. 0. 0. 1. 1. 0. 0. 0. 0. 1. 0. 0.]
#  [1. 1. 0. 0. 0. 1. 0. 1. 0. 0. 0. 0.]]
\phi(x) = \left[ \begin{aligned} 1,\mathbb{I}(x_1=S), \mathbb{I}(x_1=T),\mathbb{I}(x_1=F), \mathbb{I}(x_2=U),\mathbb{I}(x_2=J), \\ \mathbb{I}(x_1=S,x_2=U) ,\mathbb{I}(x_1=T,x_2=U),\mathbb{I}(x_1=F,x_2=U),\\ \mathbb{I}(x_1=S,x_2=J),\mathbb{I}(x_1=T,x_2=J), \mathbb{I}(x_1=F,x_2=J)  \end{aligned} \right]

Preprocessing Text Data

To feed text data into a classifier, we need to tackle various issues. First, documents have a variable length, and are thus not fixed-length feature vectors, as assumed by many kinds of models. Second, words are categorical variables with many possible values (equal to the size of the vocabulary), so the corresponding one-hot encodings will be very high-dimensional, with no natural notion of similarity. Third, we may encounter words at test time that have not been seen during training (so-called out-of-vocabulary or OOV words).

Bag of Words Model

A simple approach to dealing with variable-length text documents is to interpret them as a bag of words, in which we ignore word order. To convert this to a vector from a fixed input space, we first map each word to a token from some vocabulary.

Let xntx_{nt} be the token at location tt in the nn’th document. If there are DD unique tokens in the vocabulary, then we can represent the nn’th document as a DD-dimensional vector x~n\tilde{x}_n, where x~nv\tilde{x}_{nv} is the number of times that word vv occurs in document nn:

x~nv=t=1T𝕀(xnt=v)\tilde{x}_{nv} = \sum_{t=1}^T \mathbb{I}(x_{nt} = v)
import tensorflow as tf

text_data = [
    "The cat sat on the mat",
    "The dog lay on the rug"
]

max_tokens = 10
vectorizer = tf.keras.layers.TextVectorization(
    max_tokens=max_tokens, 
    output_mode="count"
)

vectorizer.adapt(text_data)

bow_representation = vectorizer(text_data)

print("Learned Vocabulary Indices:\n", vectorizer.get_vocabulary())
print("Abstract BoW Matrix:\n", bow_representation.numpy())
# Output: 
# Learned Vocabulary Indices:
#  ['[UNK]', np.str_('the'), np.str_('on'), np.str_('sat'), np.str_('rug'), np.str_('mat'), np.str_('lay'), np.str_('dog'), np.str_('cat')]
# Abstract BoW Matrix:
#  [[0 2 1 1 0 1 0 0 1]
#  [0 2 1 0 1 0 1 1 0]]
from sklearn.feature_extraction.text import CountVectorizer

text_data = [
    "The cat sat on the mat",
    "The dog lay on the rug"
]

vectorizer = CountVectorizer(max_features=5, lowercase=True)

bow_matrix = vectorizer.fit_transform(text_data)

print("Learned Feature Names (Vocabulary Blueprint):")
print(vectorizer.get_feature_names_out())

print("Raw Count Matrix for Training Data:")
print(bow_matrix.toarray())
# Output:
# Learned Feature Names (Vocabulary Blueprint):
#  ['cat' 'dog' 'mat' 'on' 'the']
# Raw Count Matrix for Training Data:
# [[1 0 1 1 2]
#  [0 1 0 1 2]]

TF-IDF

One problem with representing documents as word count vectors is that frequent words may have undue influence, just because the magnitude of their word count is higher, even if they do not carry much semantic content. A common solution to this is to transform the counts by taking logs, which reduces the impact of words that occur many times within a single document.

Scikit-learn
from sklearn.feature_extraction.text import TfidfVectorizer

text_data = ["The cat sat on the mat", "The dog lay on the rug"]

vectorizer = TfidfVectorizer(lowercase=True)
tfidf_matrix = vectorizer.fit_transform(text_data)

print("Vocabulary Blueprint:")
print(vectorizer.get_feature_names_out())

print("\nTF-IDF Weighted Matrix:")
print(tfidf_matrix.toarray())
# Output:
# Vocabulary Blueprint:
#  ['cat' 'dog' 'lay' 'mat' 'on' 'rug' 'sat' 'the']
# TF-IDF Weighted Matrix:
#  [[0.42519636 0.         0.         0.42519636 0.30253071 0.
#   0.42519636 0.60506143]
#  [0.         0.42519636 0.42519636 0.         0.30253071 0.42519636
#   0.         0.60506143]]

Word Embeddings

We map each sparse one-hot vector, xnt{0,1}Vx_{nt} \in \{0, 1 \} ^ V, to a lower-dimensional dense vector, entKe_{nt} \in \mathbb{R}^K using entExnte_{nt} \in \text{E}x_{nt}, where EK×V\text{E} \in \mathbb{R}^{K \times V} is learned such that semantically similar words are placed close by.

Once we have an embedding matrix, we can represent a variable-length text document as a bag of word embeddings. We can then convert this to a fixed length vector by summing (or averaging) the embeddings: en=t=1Tent=𝐄x~n\overline{e}_n = \sum_{t=1}^T e_{nt} = \mathbf{E}\tilde{x}_n, where x~n\tilde{x}_n is the bag of words representation.

We can then use this inside of a logistic regression classifier. The overall model has the form:

p(y=c|xn,θ)=softmaxn(𝐖𝐄x~n)p(y=c|x_n, \theta)=\text{softmax}_n(\mathbf{WE}\tilde{x}_n)

Dealing with Novel Words

Such novel words are bound to occur, because the set of words is an open class. For example, the set of proper nouns (names of people and places) is unbounded.

A standard heuristic to solve this problem is to replace all novel words with the special symbol UNK, which stands for “unknown”. It is better to leverage the fact that words have substructure, and then to take as input subword units or wordpieces; these are often created using a method called byte-pair encoding, which is a form of data compression that creates new symbols to represent common substrings.

Handling Missing Data

Sometimes we may have missing data, in which parts of the input xx or output yy may be unknown.

To mathematically track what is missing and what is not, we create a “mask” matrix, MM.

MM (The Mask Matrix): A binary grid of the same size as our dataset (NN examples ×D\times D features).

The Three Types of Missing Data:

Leave a Reply

Your email address will not be published. Required fields are marked *