Hidden Markov Models

Hidden Markov Models¶
Mahmood Amintoosi, Fall 2026
Computer Science Dept, Ferdowsi University of Mashhad
Source
# Auto-setup when running on Google Colab
import os
if 'google.colab' in str(get_ipython()) and not os.path.exists('/content/machine-learning'):
!git clone -q https://github.com/fum-cs/machine-learning.git /content/machine-learning
!pip --quiet install -r /content/machine-learning/requirements_colab.txt
%cd machine-learning/notebooks
# Global imports and settings
%matplotlib inline
from preamble import *
interactive = False # Set to True for interactive plots
if interactive:
fig_scale = 0.61
print_config['font.size'] = 7
print_config['xtick.labelsize'] = 5
print_config['ytick.labelsize'] = 5
plt.rcParams.update(print_config)
else: # For printing
fig_scale = 0.28
plt.rcParams.update(print_config)Hidden Markov Models (HMMs)¶
Hidden Markov Models are widely used in various fields, including natural language processing, speech recognition, and bioinformatics. They are probabilistic models that capture sequences of observations (or symbols) generated by an underlying hidden state process. A good source for HMM is Appendix A of Speech and Language Processing, by Dan Jurafsky and James H. Martin.
Here are the key components of an HMM:
States: An HMM consists of a set of hidden states. Each state represents a different situation or condition. For example, in part-of-speech tagging, states could correspond to different parts of speech (e.g., noun, verb, adjective).
Observations (Emissions): At each time step, the HMM emits an observation (symbol) based on the current hidden state. These observations can be words, phonemes, or any other relevant symbols.
Transition Probabilities: HMMs model how the hidden states transition from one to another. Transition probabilities represent the likelihood of moving from one state to another.
Emission Probabilities: Each hidden state has associated emission probabilities for generating specific observations. These probabilities indicate how likely an observation is given the current state.
The following figure shows an HMM with 4 states which can emit 2 discrete symbols or . is the probability to transition from state to state . is the probability to emit symbol in state . In this particular HMM, states can only reach themselves or the adjacent state.

The mathematical notation of HMM parameters are as follows:
Model parameters:
: The model parameters, which include the state transition probabilities, observation probabilities, and initial state probabilities.
States set
The set of all possible states .
Observation set:
The set of all possible observations.
State transition matrix (Transition Probabilities):
: The state transition matrix, where represents the probability of transitioning from state to state .
Note: In the following we may show state , by state ; hence could also demonstrated by .
Observation probabilities (Emission Probabilities):
: The observation probability matrix, where represents the probability of observing given that the system is in state .
Initial state probabilities:
: The initial state probabilities, where represents the probability of the system starting in state .
Observation sequence:
: The sequence of observations.
hidden Markov models should be characterized by three fundamental problems:
Problem 1 (Likelihood): Given an HMM and an observation sequence , determine the likelihood .
Problem 2 (Decoding): Given an observation sequence and an HMM , discover the best hidden state sequence .
Problem 3 (Learning): Given an observation sequence and the set of states in the HMM, learn the HMM parameters and .
Viterbi Algorithm¶
The Viterbi algorithm is a dynamic programming algorithm used for finding the most likely sequence of hidden states (also known as the Viterbi path) in a hidden Markov model (HMM) given a sequence of observations (problem 2). The Viterbi algorithm aims to find the most likely sequence of hidden states that maximizes the probability .
The algorithm uses the following recurrence relation:
where represents the maximum probability of the most likely sequence of states ending in state at time and observing the partial sequence .
The Viterbi algorithm can be summarized as follows:
Initialization:
, for
Recursion:
, for and
, for and
Termination:
Backtracking:
, for
The final result is the most likely sequence of hidden states and the maximum probability .
The trellis diagram is a visual representation of the Viterbi algorithm, which is used to find the most likely sequence of hidden states (also known as the Viterbi path) given an observed sequence of emissions in a Hidden Markov Model (HMM).

The trellis diagram depicts the evolution of the HMM over time, with each column representing a time step, and each row representing a possible state of the HMM. The nodes in the diagram represent the states, and the edges represent the transitions between states.
Figure: Trellis of the observation sequence ,, for the above HMM. The thick arrows indicate the most probable transitions. As an example, the transition between state at time t=2 and state at time t=3 has probability , where is the probability to be in state at time t.
The trellis diagram is constructed as follows:
The first column represents the initial state of the HMM at time . The nodes in this column correspond to the possible states of the HMM, and they are labeled with the state names ().
The subsequent columns represent the HMM at subsequent time steps (, , etc.). The nodes in these columns also correspond to the possible states of the HMM, and they are connected to the nodes in the previous column by edges that represent the state transitions.
The thickness of the edges represents the probability of the transition. The thick edges indicate the most likely transitions, as determined by the Viterbi algorithm.
In the example, the trellis diagram shows the evolution of the HMM for the observed sequence . The Viterbi algorithm is used to find the most likely sequence of hidden states that could have generated this observed sequence. The thick edges in the diagram represent the Viterbi path, which is the sequence of states that has the highest probability of producing the observed sequence.
The caption of the trellis diagram provides an example of how the transition probability between two states is calculated. Specifically, the transition probability between state S1 at time and state S4 at time is given by the expression , where:
is the probability of being in state at time
is the transition probability from state to state
is the probability of observing the symbol in state
The trellis diagram is a valuable tool for visualizing the Viterbi algorithm and understanding the most likely sequence of hidden states that could have generated the observed sequence in an HMM.
Dynamic Programming¶
Dynamic programming is a powerful technique for solving problems by breaking them down into smaller, overlapping subproblems. It’s widely used in various fields, including computer science, optimization, and image processing. One intriguing application of dynamic programming is seam carving. Seam carving is used in content-aware image resizing, in which, its algorithm is very similar to Viterbi algorithm.
Content-Aware Image Resizing¶
Content-aware image resizing aims to change the width or height of an image while preserving its essential features. Traditional methods like cropping and scaling have limitations, often resulting in loss of important content or visible artifacts. Seam carving offers an elegant solution by identifying and removing low-energy seams from the image.
What Are Seams?¶
In the context of seam carving, a seam is a connected path of pixels that spans the entire height or width of the image. The goal is to find the lowest-energy seam (i.e., the least interesting part of the image) and remove it. Seam carving ensures that the removed seam doesn’t disrupt the overall visual coherence.
Seam Carving Process¶
Energy Calculation: First, we compute the energy of each pixel in the image. Energy represents the importance of a pixel based on its color gradients. Higher energy corresponds to more visually significant regions.
Dynamic Programming: Seam carving uses dynamic programming to find the optimal seam. For vertical resizing (reducing width), we find the seam with the minimum cumulative energy from top to bottom. The seam moves left or right by at most one pixel in each row.
Seam Removal: Once we identify the lowest-energy seam, we remove it by shifting the remaining pixels to close the gap. This process is repeated until the desired image width is achieved.
Example¶
Consider the following image; Seam carving identifies the lowest-energy seam, which typically runs through less interesting areas. By removing this seam, we resize the image without sacrificing essential details.

Dynamic Programming¶
Dynamic programming plays a crucial role in both seam carving and the Viterbi algorithm. While seam carving focuses on image resizing, the Viterbi algorithm is widely used for decoding hidden Markov models (HMMs). Let’s explore their similarities and differences.
Seam Carving Algorithm¶
Energy Calculation:
Compute the energy of each pixel in the image.
Define an energy matrix where each entry represents the cumulative energy from the top-left corner to that pixel.
Dynamic Programming Table:
Create a dynamic programming table (similar to the Viterbi trellis).
Initialize the first row of the table with the energy values from the energy matrix.
Optimal Seam Identification:
For each subsequent row, compute the cumulative energy by considering the neighboring pixels (left, above-left, and above-right).
Update the dynamic programming table with the minimum cumulative energy path.
The optimal seam corresponds to the path with the lowest cumulative energy in the last row.
Seam Removal:
Remove the pixels along the identified seam.
Shift the remaining pixels to close the gap.
Repeat until the desired image width is achieved.
Seam carving and the Viterbi algorithm exemplify the versatility of dynamic programming. Whether we’re resizing images or decoding hidden states, dynamic programming provides elegant solutions.
Seam carving minimizes energy, while the Viterbi algorithm maximizes probabilities.
Both algorithms find an optimal path based on dynamic programming principles.
Seam-carving implementation¶
My first implementation of seam-carving was in MATLAB, then by Python. In the following, my new code which is a combination of my previous code and Karthik Karanth’s code is provided.
Source
"""
This is a combination of Karthik Karanth and my implementation for seam carving
"""
def calc_energy(img):
Ix, Iy = np.gradient(rgb2gray(img))
energy_map = np.abs(Ix) + np.abs(Iy)
return energy_map
def crop_c(img, num_seams):
r, c, _ = img.shape
for i in range(min([num_seams, c])):
img = carve_column(img)
return img
def crop_r(img, num_seams):
img = np.rot90(img, 1, (0, 1))
img = crop_c(img, num_seams)
img = np.rot90(img, 3, (0, 1))
return img
def carve_column(img):
r, c, _ = img.shape
M, backtrack = minimum_seam(img)
mask = np.ones((r, c), dtype=bool)
j = np.argmin(M[-1])
for i in reversed(range(r)):
mask[i, j] = False
j = backtrack[i, j]
mask = np.stack([mask] * 3, axis=2)
img = img[mask].reshape((r, c - 1, 3))
return img
def find_optimal_seam(M, backtrack):
r, c = M.shape
# Find the column index with the minimum energy in the last row
j = np.argmin(M[-1])
# Initialize an empty list to store the seam indices
seam = []
# Backtrack from the last row to the first row
for i in reversed(range(r)):
seam.append((i, j))
j = backtrack[i, j]
# Reverse the seam to get the correct order
seam.reverse()
return seam
# usage:
# seam_indices = find_optimal_seam(M, backtrack)
# Now seam_indices contains the optimal path as a list of (i, j) indicesdef minimum_seam(img):
r, c, _ = img.shape
energy_map = calc_energy(img)
M = energy_map.copy()
backtrack = np.zeros_like(M, dtype=np.int32)
for i in range(1, r):
for j in range(0, c):
left_most_col = max(0, j - 1)
idx = np.argmin(M[i - 1, left_most_col : j + 2])
backtrack[i, j] = left_most_col + idx
min_energy = M[i - 1, left_most_col + idx]
M[i, j] += min_energy
return M, backtrackUsage of the above functions for vertical or horizontal seam-carving
Source
# which_axis = "c"
# num_seams = 100
# in_filename = "img/two-butterfly.jpg"
# img = imread(in_filename)
# if which_axis == "r":
# out = crop_r(img, num_seams)
# elif which_axis == "c":
# out = crop_c(img, num_seams)
# plt.figure(figsize=(10, 5))
# plt.subplot(1, 3, 1)
# plt.axis("off")
# plt.imshow(img)
# plt.subplot(1, 3, 2)
# plt.axis("off")
# plt.imshow(calc_energy(img))
# plt.subplot(1, 3, 3)
# plt.axis("off")
# plt.imshow(out.astype(np.int16))Make the image smaller in length: 540x540 -> 400x540
in_filename = "img/MistakeFreeTomorrow.jpg"
img = imread(in_filename)
out = crop_r(img, 140)Source
if not interactive:
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
plt.axis("off")
plt.imshow(img)
plt.subplot(1, 2, 2)
plt.axis("off")
plt.imshow(out.astype(np.int16))Make the image smaller: 324x324 -> 240x240
Show the optimum seam as RED
Source
in_filename = "img/thanks.jpg"
img = imread(in_filename)
out = crop_r(img, 84)
out = crop_c(out, 84)
# The first vertical seam
M, backtrack = minimum_seam(img)
seam_indices = find_optimal_seam(M, backtrack)
im_vs = img.copy()
# Set the pixels corresponding to the seam indices to red
for i, j in seam_indices:
im_vs[i, j] = (255, 0, 0) # Set RGB values to red
# The first horizontal seam
img = np.rot90(img, 1, (0, 1))
M, backtrack = minimum_seam(img)
seam_indices = find_optimal_seam(M, backtrack)
im_hs = img.copy()
# Set the pixels corresponding to the seam indices to red
for i, j in seam_indices:
im_hs[i, j] = (255, 0, 0) # Set RGB values to red
img = np.rot90(img, 3, (0, 1))
im_hs = np.rot90(im_hs, 3, (0, 1))Source
if not interactive:
plt.figure(figsize=(10, 5))
plt.subplot(2, 3, 1)
plt.axis("off")
plt.imshow(img)
plt.subplot(2, 3, 2)
plt.axis("off")
plt.imshow(calc_energy(img))
plt.subplot(2, 3, 3)
plt.axis("off")
plt.imshow(im_vs)
plt.subplot(2, 3, 4)
plt.axis("off")
plt.imshow(out)
plt.subplot(2, 3, 5)
plt.axis("off")
plt.imshow(calc_energy(img))
plt.subplot(2, 3, 6)
plt.axis("off")
plt.imshow(im_hs)Viterbi Algorithm¶
Let’s visit the forward formula in the context of the Viterbi algorithm for Hidden Markov Models (HMMs).
Background:
The Viterbi algorithm is used to find the most likely sequence of hidden states (states of an HMM) given a sequence of observations (emissions).
In an HMM, we have:
Hidden states: (where is the total number of states).
Observations: (where is the length of the observation sequence).
Forward Algorithm:
The forward algorithm computes the probability of observing the partial sequence up to time and being in state at time . We denote this as .
The formula for is given by:
where:
represents the probability of being in state at time .
is the transition probability from state to state .
is the emission probability of observing given that we are in state .
Initialization:
To start the forward algorithm, we set for all states based on the initial state probabilities ().
Recursion:
We compute for each time step by updating it based on the previous values and the transition and emission probabilities.
Termination:
The final probability of the entire observation sequence is given by .
The forward algorithm recursively computes the probabilities of being in different states at each time step, considering both transitions and emissions.

Let’s compute , which represents the probability of starting in state 1 and observing ; using the given formula ():
, where is the initial state probability for state j and is the emission probability for observation given that we are in state .

from hidden_markov import hmmDefining the HMM model of page 37 of Dr. Veisi’s slides
# Define the Model
states = ("S1", "S2")
observations = ("A", "B")
start_prob_matrix = np.matrix([1, 0])
transition_matrix = np.matrix([[0.6, 0.4], [0, 1]])
emission_matrix = np.matrix([[0.8, 0.2], [0.3, 0.7]])hmm_model = hmm(
states, observations, start_prob_matrix, transition_matrix, emission_matrix
)# The observations that we observe and feed to the model
obs1 = ("A", "A", "B")
hmm_model.forward_algo(obs1)0.22080000000000002hmm_model.viterbi(obs1)['S1', 'S1', 'S2']Define another model with dictionaries GfG
# Define the Model
states = ("Rainy", "Sunny")
observations = ("walk", "shop", "clean")
start_probability = {"Rainy": 0.6, "Sunny": 0.4}
transition_probability = {
"Rainy": {"Rainy": 0.7, "Sunny": 0.3},
"Sunny": {"Rainy": 0.4, "Sunny": 0.6},
}
emission_probability = {
"Rainy": {"walk": 0.1, "shop": 0.4, "clean": 0.5},
"Sunny": {"walk": 0.6, "shop": 0.3, "clean": 0.1},
}Converting dictionary to matrix
Source
# Converting dictionary to matrix
n = len(states)
start_prob_matrix = np.matrix(list(start_probability.values()))
# print(start_prob_matrix)
transition_matrix = np.zeros((n, n))
for i, row_state in enumerate(states):
for j, col_state in enumerate(states):
transition_matrix[i, j] = transition_probability[row_state][col_state]
transition_matrix = np.matrix(transition_matrix)
# print(transition_matrix)
rows = len(emission_probability)
cols = len(emission_probability["Rainy"])
emission_matrix = np.zeros((rows, cols))
for i, state in enumerate(emission_probability):
for j, activity in enumerate(emission_probability[state]):
emission_matrix[i][j] = emission_probability[state][activity]
emission_matrix = np.matrix(emission_matrix)
# print(emission_matrix)hmm_model = hmm(
states, observations, start_prob_matrix, transition_matrix, emission_matrix
)# The observations that we observe and feed to the model
obs1 = ("walk", "shop", "clean")
hmm_model.forward_algo(obs1)0.033612hmm_model.viterbi(obs1)['Sunny', 'Rainy', 'Rainy']References:
Speech and Language Processing, by Dan Jurafsky and James H. Martin. Stanford University, 2023.
Shai Avidan and Ariel Shamir. “Seam Carving for Content-Aware Image Resizing.”
Implementing Seam Carving with Python, by Karthik Karanth
Intro to the Seam Carving Algorithm, by Ben Tanen
Intro to the Viterbi Algorithm, by Paul Butler