Skip to content
AI360Xpert
Beta

Policy Iteration

Policy iteration alternates between computing the exact value of the current policy and updating that policy to act greedily with respect to those values. Repeating this cycle guarantees reaching the strictly optimal policy in a finite number of iterations.

Policy iteration loops between full policy evaluation and greedy policy improvement until the policy stops changing, guaranteeing convergence to the optimal policy.
Policy iteration loops between full policy evaluation and greedy policy improvement until the policy stops changing, guaranteeing convergence to the optimal policy.

Why Does This Exist?

In a known Markov Decision Process (MDP), the goal is to discover an optimal policy π∗\pi^* that maximizes expected cumulative discounted returns. However, searching through all possible policies is a combinatorial nightmare: for a discrete MDP with state space SS and action space AA, there are ∣A∣∣S∣|A|^{|S|} distinct deterministic policies. In a simple grid with only 50 states and 4 movement directions, brute-force evaluation must inspect 450≈1.27×10304^{50} \approx 1.27 \times 10^{30} policies—a computational impossibility.

Evaluating an existing policy is a tractable linear problem, but finding the globally optimal policy directly is non-linear because choices made in one state dictate the values of upstream states. Without a systematic mechanism to navigate the space of policies, algorithms either stall in local heuristics or resort to intractable brute-force enumeration.

Policy iteration, introduced by Ronald Howard in 1960, solves this by decomposing control into two alternating sub-problems:

  1. Policy Evaluation: Compute the exact long-term value function vπv_\pi for the agent's current policy π\pi.
  2. Policy Improvement: Construct an improved policy π′\pi' by acting greedily with respect to the freshly calculated value landscape vπv_\pi.

The mathematical engine powering this loop is the Policy Improvement Theorem. It guarantees that each greedy update produces a policy that is strictly better than the previous one across all states, or identical if the policy is already globally optimal. Because finite MDPs possess a finite number of policies and no policy can ever repeat, policy iteration provably terminates at the exact globally optimal policy π∗\pi^* in a finite number of iterations.

Think of It Like This

Drafting and polishing an encyclopedia manuscript

Imagine an author collaborating with an editorial committee to produce a definitive technical handbook:

  1. Full Evaluation (The Comprehensive Editorial Audit): The author begins with a rough initial outline (π0\pi_0). Before suggesting revisions, the editors read the entire manuscript from cover to cover. They calculate the exact pedagogical impact and clarity score of every single section under the current text (evaluating vπ0v_{\pi_0} to absolute convergence).
  2. Policy Improvement (Targeted Chapter Revisions): With the complete scorecard in hand, the author inspects each chapter one by one. Whenever an alternative explanation or better case study yields a higher expected reader score based on the editorial audit, the author greedily substitutes that chapter's text (forming a revised draft π1\pi_1).

The author hands draft π1\pi_1 back to the committee. Because every substituted chapter was chosen based on exact evaluations, the Policy Improvement Theorem guarantees that draft π1\pi_1 is at least as good as, and typically much stronger than, draft π0\pi_0. When the editors complete an audit and cannot find a single chapter revision that yields a higher score (πk+1=πk\pi_{k+1} = \pi_k), the handbook has reached its optimal version.

Where the analogy stops: Human writing involves subjective taste, stylistic ambiguity, and diminishing author stamina. In an MDP with discount factor γ<1\gamma < 1, mathematical evaluation is strictly objective, monotonic, and mathematically bound to terminate at the unique global optimum without getting trapped in cyclical edits.

How It Actually Works

The Evaluation-Improvement Loop and Convergence Guarantees

Policy iteration alternates between two foundational operators in a closed loop:

π0→Evaluationvπ0→Improvementπ1→Evaluationvπ1→Improvement⋯→Stableπ∗\pi_0 \xrightarrow{\text{Evaluation}} v_{\pi_0} \xrightarrow{\text{Improvement}} \pi_1 \xrightarrow{\text{Evaluation}} v_{\pi_1} \xrightarrow{\text{Improvement}} \dots \xrightarrow{\text{Stable}} \pi^*

1. Policy Evaluation (EE)

Given a policy πk\pi_k, policy evaluation calculates the expected return vπk(s)v_{\pi_k}(s) for every state s∈Ss \in S. The value function satisfies the linear Bellman expectation equation:

vπk(s)=∑a∈Aπk(a∣s)∑s′∈SP(s′∣s,a)[R(s,a,s′)+γvπk(s′)]v_{\pi_k}(s) = \sum_{a \in A} \pi_k(a \mid s) \sum_{s' \in S} P(s' \mid s, a) \left[ R(s, a, s') + \gamma v_{\pi_k}(s') \right]

For deterministic policies where πk(s)\pi_k(s) selects a single action:

vπk(s)=∑s′∈SP(s′∣s,πk(s))[R(s,πk(s),s′)+γvπk(s′)]v_{\pi_k}(s) = \sum_{s' \in S} P(s' \mid s, \pi_k(s)) \left[ R(s, \pi_k(s), s') + \gamma v_{\pi_k}(s') \right]

This forms a system of ∣S∣|S| linear equations with ∣S∣|S| unknowns. It can be solved either directly in matrix form via (I−γPπk)vπk=rπk(I - \gamma P_{\pi_k}) v_{\pi_k} = r_{\pi_k}, or iteratively through successive relaxation sweeps:

v(m+1)(s)←∑s′P(s′∣s,πk(s))[R(s,πk(s),s′)+γv(m)(s′)]v^{(m+1)}(s) \leftarrow \sum_{s'} P(s' \mid s, \pi_k(s)) \left[ R(s, \pi_k(s), s') + \gamma v^{(m)}(s') \right]

The sweeps repeat until max⁡s∈S∣v(m+1)(s)−v(m)(s)∣<θ\max_{s \in S} |v^{(m+1)}(s) - v^{(m)}(s)| < \theta, where θ\theta is a small positive convergence threshold.

2. Policy Improvement (II)

Once the exact value function vπkv_{\pi_k} is known, the agent evaluates the action-value function qπk(s,a)q_{\pi_k}(s, a) for every possible action a∈Aa \in A:

qπk(s,a)=∑s′∈SP(s′∣s,a)[R(s,a,s′)+γvπk(s′)]q_{\pi_k}(s, a) = \sum_{s' \in S} P(s' \mid s, a) \left[ R(s, a, s') + \gamma v_{\pi_k}(s') \right]

The new policy πk+1\pi_{k+1} is extracted greedily by choosing the action that maximizes this one-step lookahead:

πk+1(s)=argmax⁡a∈Aqπk(s,a)\pi_{k+1}(s) = \operatorname{argmax}_{a \in A} q_{\pi_k}(s, a)

If multiple actions tie for the maximum, the agent may pick any maximizing action deterministically or split selection probability equally.

3. The Policy Improvement Theorem and Finite Convergence

The theoretical backbone of the algorithm is the Policy Improvement Theorem: If qπk(s,πk+1(s))≥vπk(s)q_{\pi_k}(s, \pi_{k+1}(s)) \ge v_{\pi_k}(s) for all s∈Ss \in S, then:

vπk+1(s)≥vπk(s)∀s∈Sv_{\pi_{k+1}}(s) \ge v_{\pi_k}(s) \quad \forall s \in S

If the inequality is strict in at least one state, the entire policy is strictly superior. If no action change occurs—meaning πk+1(s)=πk(s)\pi_{k+1}(s) = \pi_k(s) across all states—the policy is stable. At this point, the greedy action selection satisfies:

vπk(s)=max⁡a∈A∑s′P(s′∣s,a)[R(s,a,s′)+γvπk(s′)]v_{\pi_k}(s) = \max_{a \in A} \sum_{s'} P(s' \mid s, a) \left[ R(s, a, s') + \gamma v_{\pi_k}(s') \right]

This is the Bellman Optimality Equation. Therefore, vπk=v∗v_{\pi_k} = v^* and πk=π∗\pi_k = \pi^*.

Because an MDP with finite state space SS and finite action space AA has at most ∣A∣∣S∣|A|^{|S|} unique deterministic policies, and each iteration strictly increases the value function unless already optimal, no policy can ever be evaluated twice. Consequently, policy iteration is guaranteed to terminate in at most ∣A∣∣S∣|A|^{|S|} outer iterations. In practice, it converges in a tiny fraction of that bound (often fewer than 10 iterations even on complex problems).


Worked numerical example

Consider a 3-state chain MDP:

  • States: S={s1,s2,s3}S = \{s_1, s_2, s_3\}. State s3s_3 is a terminal absorbing state with fixed value v(s3)=0v(s_3) = 0.
  • Actions: A={L (Left),R (Right)}A = \{\text{L (Left)}, \text{R (Right)}\}.
  • Discount factor: γ=0.5\gamma = 0.5.
  • Transition Dynamics & Rewards:
    • In s1s_1: action L\text{L} loops back to s1s_1 with reward R=0R = 0. Action R\text{R} moves to s2s_2 with reward R=1R = 1.
    • In s2s_2: action L\text{L} moves to s1s_1 with reward R=0R = 0. Action R\text{R} transitions to terminal s3s_3 with reward R=10R = 10.
    • In terminal s3s_3: absorbing state, all actions yield reward 0.

Cycle 0: Initial Policy π0\pi_0

We initialize an arbitrary policy choosing L\text{L} everywhere: π0(s1)=L,π0(s2)=L\pi_0(s_1) = \text{L}, \quad \pi_0(s_2) = \text{L}

Step 0.1: Policy Evaluation of π0\pi_0 Set up the Bellman expectation equations:

  • v(s1)=R(s1,L)+γv(s1)=0+0.5⋅v(s1)  ⟹  0.5v(s1)=0  ⟹  vπ0(s1)=0.0v(s_1) = R(s_1, \text{L}) + \gamma v(s_1) = 0 + 0.5 \cdot v(s_1) \implies 0.5 v(s_1) = 0 \implies v_{\pi_0}(s_1) = 0.0
  • v(s2)=R(s2,L)+γv(s1)=0+0.5⋅(0.0)=0.0  ⟹  vπ0(s2)=0.0v(s_2) = R(s_2, \text{L}) + \gamma v(s_1) = 0 + 0.5 \cdot (0.0) = 0.0 \implies v_{\pi_0}(s_2) = 0.0

vπ0=[v(s1)=0.0,  v(s2)=0.0]v_{\pi_0} = [v(s_1) = 0.0, \; v(s_2) = 0.0]

Step 0.2: Policy Improvement on π0\pi_0 Evaluate action-values q(s,a)=R+γv(s′)q(s, a) = R + \gamma v(s'):

  • For state s1s_1: q(s1,L)=0+0.5⋅v(s1)=0+0.5(0.0)=0.0q(s_1, \text{L}) = 0 + 0.5 \cdot v(s_1) = 0 + 0.5(0.0) = 0.0 q(s1,R)=1+0.5⋅v(s2)=1+0.5(0.0)=1.0q(s_1, \text{R}) = 1 + 0.5 \cdot v(s_2) = 1 + 0.5(0.0) = 1.0 π1(s1)=argmax⁡a{0.0,1.0}=R(switched from L)\pi_1(s_1) = \operatorname{argmax}_a \{0.0, 1.0\} = \text{R} \quad (\text{switched from L})

  • For state s2s_2: q(s2,L)=0+0.5⋅v(s1)=0+0.5(0.0)=0.0q(s_2, \text{L}) = 0 + 0.5 \cdot v(s_1) = 0 + 0.5(0.0) = 0.0 q(s2,R)=10+0.5⋅v(s3)=10+0.5(0.0)=10.0q(s_2, \text{R}) = 10 + 0.5 \cdot v(s_3) = 10 + 0.5(0.0) = 10.0 π1(s2)=argmax⁡a{0.0,10.0}=R(switched from L)\pi_1(s_2) = \operatorname{argmax}_a \{0.0, 10.0\} = \text{R} \quad (\text{switched from L})

Policy changed from [L,L][\text{L}, \text{L}] to [R,R][\text{R}, \text{R}]. Policy is not stable. Proceed to Cycle 1.


Cycle 1: Policy π1=[R,R]\pi_1 = [\text{R}, \text{R}]

Step 1.1: Policy Evaluation of π1\pi_1 Evaluate values under the newly proposed policy:

  • In s2s_2: action R\text{R} transitions to terminal s3s_3: vπ1(s2)=10+0.5⋅v(s3)=10+0.5(0.0)=10.0v_{\pi_1}(s_2) = 10 + 0.5 \cdot v(s_3) = 10 + 0.5(0.0) = 10.0
  • In s1s_1: action R\text{R} transitions to s2s_2: vπ1(s1)=1+0.5⋅vπ1(s2)=1+0.5(10.0)=1+5.0=6.0v_{\pi_1}(s_1) = 1 + 0.5 \cdot v_{\pi_1}(s_2) = 1 + 0.5(10.0) = 1 + 5.0 = 6.0

vπ1=[v(s1)=6.0,  v(s2)=10.0]v_{\pi_1} = [v(s_1) = 6.0, \; v(s_2) = 10.0]

Notice the monotonic value surge: vπ1(s1)=6.0>vπ0(s1)=0.0v_{\pi_1}(s_1) = 6.0 > v_{\pi_0}(s_1) = 0.0 and vπ1(s2)=10.0>vπ0(s2)=0.0v_{\pi_1}(s_2) = 10.0 > v_{\pi_0}(s_2) = 0.0.

Step 1.2: Policy Improvement on π1\pi_1 Re-compute action-values using the new value landscape:

  • For state s1s_1: q(s1,L)=0+0.5⋅v(s1)=0+0.5(6.0)=3.0q(s_1, \text{L}) = 0 + 0.5 \cdot v(s_1) = 0 + 0.5(6.0) = 3.0 q(s1,R)=1+0.5⋅v(s2)=1+0.5(10.0)=6.0q(s_1, \text{R}) = 1 + 0.5 \cdot v(s_2) = 1 + 0.5(10.0) = 6.0 π2(s1)=argmax⁡a{3.0,6.0}=R(unchanged)\pi_2(s_1) = \operatorname{argmax}_a \{3.0, 6.0\} = \text{R} \quad (\text{unchanged})

  • For state s2s_2: q(s2,L)=0+0.5⋅v(s1)=0+0.5(6.0)=3.0q(s_2, \text{L}) = 0 + 0.5 \cdot v(s_1) = 0 + 0.5(6.0) = 3.0 q(s2,R)=10+0.5⋅v(s3)=10+0.5(0.0)=10.0q(s_2, \text{R}) = 10 + 0.5 \cdot v(s_3) = 10 + 0.5(0.0) = 10.0 π2(s2)=argmax⁡a{3.0,10.0}=R(unchanged)\pi_2(s_2) = \operatorname{argmax}_a \{3.0, 10.0\} = \text{R} \quad (\text{unchanged})

Updated policy π2=[R,R]\pi_2 = [\text{R}, \text{R}]. Because π2(s)=π1(s)\pi_2(s) = \pi_1(s) for all states, the policy is stable. The algorithm terminates.

Final Optimal Solution:

  • Optimal Policy: π∗(s1)=R,  π∗(s2)=R\pi^*(s_1) = \text{R}, \; \pi^*(s_2) = \text{R}
  • Optimal Values: v∗(s1)=6.0,  v∗(s2)=10.0v^*(s_1) = 6.0, \; v^*(s_2) = 10.0

Code

from typing import Dict, List, Tuple
# Type aliases representing discrete MDP components# State -> Action -> list of (transition_probability, next_state, immediate_reward)MDPTransitions = Dict[str, Dict[str, List[Tuple[float, str, float]]]]Policy = Dict[str, str]ValueFunction = Dict[str, float]

def policy_evaluation(    policy: Policy,    transitions: MDPTransitions,    gamma: float = 0.5,    theta: float = 1e-9,) -> ValueFunction:    """Computes exact state values V_pi for a deterministic policy via Bellman sweeps.
    Args:        policy: Mapping of each state to its selected action.        transitions: MDP transition dynamics and reward mapping.        gamma: Discount factor in range [0, 1).        theta: Convergence threshold for stopping iterative sweeps.
    Returns:        Converged state-value function mapping each state to its expected return.    """    states = list(transitions.keys())    V: ValueFunction = {s: 0.0 for s in states}
    while True:        delta = 0.0        for s in states:            chosen_action = policy[s]            # Bellman expectation equation:            # V(s) = sum_{s'} P(s'|s, a) * [R(s, a, s') + gamma * V(s')]            expected_val = sum(                prob * (reward + gamma * V[next_s])                for prob, next_s, reward in transitions[s][chosen_action]            )            delta = max(delta, abs(expected_val - V[s]))            V[s] = expected_val
        if delta < theta:            break
    return V

def policy_improvement(    V: ValueFunction,    transitions: MDPTransitions,    current_policy: Policy,    gamma: float = 0.5,) -> Tuple[Policy, bool]:    """Greedily extracts a new policy with respect to the value function V.
    Args:        V: Evaluated state values.        transitions: MDP transition dynamics and reward mapping.        current_policy: The policy that generated V.        gamma: Discount factor in range [0, 1).
    Returns:        A tuple of (new_policy, is_stable).    """    new_policy: Policy = {}    policy_stable = True
    for s, available_actions in transitions.items():        q_values: Dict[str, float] = {}
        for a, outcomes in available_actions.items():            # Action-value Q(s, a) lookahead            q_values[a] = sum(                prob * (reward + gamma * V[next_s])                for prob, next_s, reward in outcomes            )
        # Greedily choose action maximizing Q(s, a)        best_action = max(q_values, key=lambda action: q_values[action])        new_policy[s] = best_action
        if best_action != current_policy[s]:            policy_stable = False
    return new_policy, policy_stable

def policy_iteration(    transitions: MDPTransitions,    gamma: float = 0.5,    theta: float = 1e-9,) -> Tuple[Policy, ValueFunction, int]:    """Runs the full Policy Iteration algorithm until policy convergence.
    Returns:        A tuple of (optimal_policy, optimal_values, total_cycles).    """    # Initialize with an arbitrary deterministic policy (first action in each state)    policy: Policy = {s: list(transitions[s].keys())[0] for s in transitions}    cycles = 0
    while True:        cycles += 1        # Step 1: Policy Evaluation        V = policy_evaluation(policy, transitions, gamma, theta)
        # Step 2: Policy Improvement        new_policy, is_stable = policy_improvement(V, transitions, policy, gamma)
        policy = new_policy        if is_stable:            break
    return policy, V, cycles

if __name__ == "__main__":    # 3-state chain MDP from the worked numerical example:    # State s3 is absorbing (terminal) with 0 return.    mdp: MDPTransitions = {        "s1": {            "L": [(1.0, "s1", 0.0)],            "R": [(1.0, "s2", 1.0)],        },        "s2": {            "L": [(1.0, "s1", 0.0)],            "R": [(1.0, "s3", 10.0)],        },        "s3": {            "L": [(1.0, "s3", 0.0)],            "R": [(1.0, "s3", 0.0)],        },    }
    optimal_policy, optimal_values, num_cycles = policy_iteration(mdp, gamma=0.5)
    print(f"Optimal Policy: s1 -> {optimal_policy['s1']}, s2 -> {optimal_policy['s2']}")    print(f"Optimal Values: V(s1) = {optimal_values['s1']:.1f}, V(s2) = {optimal_values['s2']:.1f}")    print(f"Cycles to Convergence: {num_cycles}")
    # Expected output:    # Optimal Policy: s1 -> R, s2 -> R    # Optimal Values: V(s1) = 6.0, V(s2) = 10.0    # Cycles to Convergence: 2

Watch Out For

Over-evaluating early, rapidly changing policies

The most common trap in exact policy iteration is computing policy evaluation to tight numerical tolerance (Δ<10−9\Delta < 10^{-9}) in early outer cycles.

Symptom: The algorithm spends 95% to 99% of its total execution time inside the inner evaluation loop solving for values of an unstable policy that will flip its actions in the very next greedy improvement pass. As state spaces grow, this manifests as extreme latency between policy updates.

Root cause: Full policy evaluation requires solving a system of linear equations of size ∣S∣|S|, either via an O(∣S∣3)O(|S|^3) direct matrix inversion or dozens of iterative Bellman expectation sweeps. In the early iterations, knowing the precise decimal values is redundant; all that matters for improvement is whether action a1a_1 scores higher than action a2a_2.

The fix: Use Truncated Policy Evaluation (also known as Modified Policy Iteration). Cap the inner evaluation loop at a fixed number of sweeps (e.g., m=3m = 3 to m=10m = 10 iterations), or use an adaptive threshold θk\theta_k that starts loose and tightens as fewer policy actions flip. In the extreme case of m=1m = 1, this converges directly into Value Iteration.

The Quick Version

  • Alternating engine: Policy iteration alternates between full policy evaluation (vπkv_{\pi_k}) and greedy policy improvement (πk+1(s)=argmax⁡aq(s,a)\pi_{k+1}(s) = \operatorname{argmax}_a q(s, a)).
  • Monotonic improvement: By the Policy Improvement Theorem, each greedy step guarantees vπk+1(s)≥vπk(s)v_{\pi_{k+1}}(s) \ge v_{\pi_k}(s) across every state, eliminating regression.
  • Finite convergence: Termination triggers when no action flips (πk+1=πk\pi_{k+1} = \pi_k), provably reaching the unique optimal policy π∗\pi^* in at most ∣A∣∣S∣|A|^{|S|} outer iterations.
  • Computation tradeoff: Policy iteration makes dramatic policy improvements in very few outer cycles compared to value iteration, but incurs high per-cycle cost from inner evaluation loops.