Skip to content
AI360Xpert
Beta

Prioritized Sweeping

Prioritized sweeping focuses model planning updates on state-action pairs with the largest Bellman error, dramatically accelerating value propagation. Rather than sampling transitions uniformly, it maintains a priority queue that ripples major rewards backward through predecessor states.

Prioritized sweeping maintains a priority queue of Bellman errors, rippling value updates backward along predecessor states.
Prioritized sweeping maintains a priority queue of Bellman errors, rippling value updates backward along predecessor states.

Why Does This Exist?

In model-based reinforcement learning, planning algorithms simulate experiences from an environmental model to accelerate value convergence without demanding expensive real-world interactions. In classical architectures like Dyna-Q, simulated transitions are chosen by uniform random sampling over all previously experienced state-action pairs.

In environments with non-trivial state spaces, uniform random planning becomes catastrophically inefficient:

  1. Wasted Computation on Quiescent States: In a maze or gridworld with hundreds of states, 95% or more of state transitions have zero Bellman error—their values are already consistent with their successors. Uniform sampling blindly wastes precious planning cycles computing updates where δ=0.0\delta = 0.0.
  2. Sluggish Diffusion of Goal Surprises: When an agent first stumbles upon a large reward at a goal state, that information must propagate back to the start state. Under uniform sampling, the probability that the model randomly selects the state immediately preceding the goal is small. The probability that it subsequently selects the state two steps prior is smaller still. Propagating a goal signal across kk steps requires on the order of O(∣S∣k)\mathcal{O}(|\mathcal{S}|^k) random simulated steps.

Why update transitions at random when we know exactly which states changed?

Introduced independently by Andrew Moore and Christopher Atkeson (1993) and by Jing Peng and Ronald Leisenring (1996), Prioritized Sweeping replaces blind uniform sampling with a priority queue ordered by Bellman error magnitude:

P=∣R+γmax⁡a′Q(S′,a′)−Q(S,A)∣P = \left| R + \gamma \max_{a'} Q(S', a') - Q(S, A) \right|

When a major reward or unexpected transition occurs, the algorithm inserts the affected state-action pair into the priority queue. During planning, it pops the highest-priority pair, updates its value, and then works backward: it queries the model for all predecessor state-action pairs that lead into that updated state, computes their anticipated errors, and inserts them into the queue. A single high-impact discovery ripples backward through the state graph directly toward the initial state in a focused wave of updates.

Think of It Like This

Emergency Dispatch Routing in a Major City

Imagine managing an urban emergency response network across hundreds of square miles:

  • The Dyna-Q Approach (Random Street Patrols): Emergency vehicles randomly patrol suburban cul-de-sacs where nothing has occurred for months. When a massive 5-alarm fire erupts at a chemical plant downtown (a goal reward discovery), dispatch continues telling units to cruise at random. The system hopes that a patrol car will coincidentally wander downtown, notice the fire, and then later wander toward fire stations to notify them. The response is disorganized, diffuse, and painfully slow.
  • The Prioritized Sweeping Approach (Priority Dispatch Queue): Dispatch maintains an active priority queue sorted strictly by emergency urgency (Bellman error). The chemical plant explosion immediately enters the top of the queue with priority 10.
  • The Backward Ripple Effect: The moment fire crews respond and contain the epicenter (the goal transition is updated), dispatch does not return to random cruising. It immediately looks up the predecessor arterial avenues and highway exits feeding into downtown (predecessor states). Because traffic is backing up, those feeder junctions are assigned high priority (priority 9.0) and emergency units are routed there next.
  • Once those arteries are stabilized, the outer ring-roads feeding them receive priority 8.1. The coordination ripples backward along the logistical transport chain directly to where commuters depart, while quiet residential neighborhoods remain untouched.

Where the analogy breaks down: Urban traffic involves continuous fluid dynamics, whereas Prioritized Sweeping operates over discrete Markov transition graphs with explicit predecessor maps. Furthermore, in Prioritized Sweeping, if an update falls below a numerical threshold θ\theta, it is completely discarded from the queue, whereas emergency dispatch must eventually attend to minor calls.

How It Actually Works

Priority Queue Planning and Backward Ripple Dynamics

Prioritized Sweeping combines an empirical transition model with a max-priority queue (typically implemented with a binary heap) to drive value iteration updates:

1. The Model and Predecessor Graph

The agent maintains two lookup tables based on real-world transitions (St,At,Rt+1,St+1)(S_t, A_t, R_{t+1}, S_{t+1}):

  • Forward Model: Stores deterministic or expected transitions: Model(S,A)→(R,S′)Model(S, A) \to (R, S')
  • Predecessor Registry: Maps every successor state s′s' to the set of state-action pairs that have historically led to it: Pred(S′)={(S,A)∣Model(S,A) transitions to S′}Pred(S') = \left\{ (S, A) \mid Model(S, A) \text{ transitions to } S' \right\}

2. The Priority Metric

The priority PP of any state-action pair (S,A)(S, A) is the absolute magnitude of its one-step Bellman error:

P≐∣R+γmax⁡a′Q(S′,a′)−Q(S,A)∣P \doteq \left| R + \gamma \max_{a'} Q(S', a') - Q(S, A) \right|

where:

  • γ∈[0,1]\gamma \in [0, 1] is the discount factor.
  • Q(S,A)Q(S, A) is the current action-value estimate.
  • RR and S′S' are the predicted reward and next state from Model(S,A)Model(S, A).

If P>θP > \theta (where θ>0\theta > 0 is a small positive threshold), (S,A)(S, A) is inserted into the priority queue with priority PP. If (S,A)(S, A) already resides in the queue, its priority is increased to max⁡(Pold,P)\max(P_{\text{old}}, P).

3. The Planning Loop

After taking a real step, the agent performs up to nn planning iterations while the priority queue is non-empty:

  1. Pop the Top Priority: Remove (S,A)(S, A) with the largest priority PP from the queue: (S,A)←pop_max(PQueue)(S, A) \leftarrow \text{pop\_max}(PQueue)
  2. Execute Model Backup: Retrieve (R,S′)=Model(S,A)(R, S') = Model(S, A) and update its action-value: Q(S,A)←Q(S,A)+α[R+γmax⁡a′Q(S′,a′)−Q(S,A)]Q(S, A) \leftarrow Q(S, A) + \alpha \left[ R + \gamma \max_{a'} Q(S', a') - Q(S, A) \right] (In deterministic environments, setting α=1.0\alpha = 1.0 sets Q(S,A)Q(S, A) exactly to its updated target, reducing its Bellman error to zero).
  3. Propagate Backward to Predecessors: For every predecessor pair (Sˉ,Aˉ)∈Pred(S)(\bar{S}, \bar{A}) \in Pred(S) known to transition into SS:
    • Retrieve its predicted reward Rˉ\bar{R} from Model(Sˉ,Aˉ)Model(\bar{S}, \bar{A}).
    • Compute the anticipated Bellman error for the predecessor: Pˉ≐∣Rˉ+γmax⁡aQ(S,a)−Q(Sˉ,Aˉ)∣\bar{P} \doteq \left| \bar{R} + \gamma \max_a Q(S, a) - Q(\bar{S}, \bar{A}) \right|
    • If Pˉ>θ\bar{P} > \theta, insert or promote (Sˉ,Aˉ)(\bar{S}, \bar{A}) in the priority queue with priority Pˉ\bar{P}.

4. Quiescence and Convergence

Unlike Dyna-Q, which always runs exactly nn simulated steps regardless of whether they are needed, Prioritized Sweeping terminates early if the priority queue becomes empty. When all errors in the network drop below threshold θ\theta, planning stops naturally, saving computational cycles until another surprise occurs.

Worked numerical example

Consider a 3-state linear chain leading to a terminal goal state:

S1→rightS2→rightS3→rightS4(Goal)S_1 \xrightarrow{\text{right}} S_2 \xrightarrow{\text{right}} S_3 \xrightarrow{\text{right}} S_4 (\text{Goal})

System Parameters

  • Discount factor: γ=0.9\gamma = 0.9
  • Step size: α=1.0\alpha = 1.0 (full Bellman backup)
  • Priority threshold: θ=0.01\theta = 0.01
  • Planning capacity: n=10n = 10 steps

Model State

The agent has already traversed S1→S2S_1 \to S_2 and S2→S3S_2 \to S_3 with zero reward, establishing the model and predecessor registry:

  • Model(S1,right)=(0.0,S2)  ⟹  Pred(S2)={(S1,right)}Model(S_1, \text{right}) = (0.0, S_2) \implies Pred(S_2) = \{(S_1, \text{right})\}
  • Model(S2,right)=(0.0,S3)  ⟹  Pred(S3)={(S2,right)}Model(S_2, \text{right}) = (0.0, S_3) \implies Pred(S_3) = \{(S_2, \text{right})\}
  • Initial values: Q(S1,right)=0.0,Q(S2,right)=0.0,Q(S3,right)=0.0Q(S_1, \text{right}) = 0.0, Q(S_2, \text{right}) = 0.0, Q(S_3, \text{right}) = 0.0.
  • Terminal state S4S_4 has max⁡aQ(S4,a)=0.0\max_a Q(S_4, a) = 0.0.

Event: Goal Discovery at S3S_3

The agent executes (S3,right)(S_3, \text{right}), observes reward R=+10.0R = +10.0, and reaches terminal state S4S_4.

  1. Model Update: Model(S3,right)=(10.0,S4)Model(S_3, \text{right}) = (10.0, S_4) and Pred(S4)={(S3,right)}Pred(S_4) = \{(S_3, \text{right})\}.
  2. Initial Bellman Error: P=∣10.0+0.9×0.0−0.0∣=10.0000P = |10.0 + 0.9 \times 0.0 - 0.0| = 10.0000
  3. Queue Insertion: Because 10.0>0.0110.0 > 0.01, insert (S3,right)(S_3, \text{right}) into PQueuePQueue with priority P=10.0000P = 10.0000.

Planning Iteration 1

  1. Pop: Top element is (S3,right)(S_3, \text{right}) with priority 10.000010.0000.
  2. Update Q(S3,right)Q(S_3, \text{right}): Q(S3,right)←0.0+1.0×[10.0+0.9×0.0−0.0]=10.0000Q(S_3, \text{right}) \leftarrow 0.0 + 1.0 \times [10.0 + 0.9 \times 0.0 - 0.0] = 10.0000
  3. Backward Lookup: Predecessors of S3S_3 is Pred(S3)={(S2,right)}Pred(S_3) = \{(S_2, \text{right})\}.
    • For (S2,right)(S_2, \text{right}), predicted reward is Rˉ=0.0\bar{R} = 0.0 and next state is S3S_3.
    • Compute predecessor priority: Pˉ=∣0.0+0.9×max⁡Q(S3)−Q(S2,right)∣=∣0.0+0.9×10.0−0.0∣=9.0000\bar{P} = |0.0 + 0.9 \times \max Q(S_3) - Q(S_2, \text{right})| = |0.0 + 0.9 \times 10.0 - 0.0| = 9.0000
    • Since 9.0000>0.019.0000 > 0.01, insert (S2,right)(S_2, \text{right}) into PQueuePQueue with priority 9.00009.0000.

Planning Iteration 2

  1. Pop: Top element is (S2,right)(S_2, \text{right}) with priority 9.00009.0000.
  2. Update Q(S2,right)Q(S_2, \text{right}): Q(S2,right)←0.0+1.0×[0.0+0.9×10.0−0.0]=9.0000Q(S_2, \text{right}) \leftarrow 0.0 + 1.0 \times [0.0 + 0.9 \times 10.0 - 0.0] = 9.0000
  3. Backward Lookup: Predecessors of S2S_2 is Pred(S2)={(S1,right)}Pred(S_2) = \{(S_1, \text{right})\}.
    • For (S1,right)(S_1, \text{right}), predicted reward is Rˉ=0.0\bar{R} = 0.0 and next state is S2S_2.
    • Compute predecessor priority: Pˉ=∣0.0+0.9×max⁡Q(S2)−Q(S1,right)∣=∣0.0+0.9×9.0−0.0∣=8.1000\bar{P} = |0.0 + 0.9 \times \max Q(S_2) - Q(S_1, \text{right})| = |0.0 + 0.9 \times 9.0 - 0.0| = 8.1000
    • Since 8.1000>0.018.1000 > 0.01, insert (S1,right)(S_1, \text{right}) into PQueuePQueue with priority 8.10008.1000.

Planning Iteration 3

  1. Pop: Top element is (S1,right)(S_1, \text{right}) with priority 8.10008.1000.
  2. Update Q(S1,right)Q(S_1, \text{right}): Q(S1,right)←0.0+1.0×[0.0+0.9×9.0−0.0]=8.1000Q(S_1, \text{right}) \leftarrow 0.0 + 1.0 \times [0.0 + 0.9 \times 9.0 - 0.0] = 8.1000
  3. Backward Lookup: S1S_1 has no predecessors (Pred(S1)=∅Pred(S_1) = \emptyset).
  4. Queue Status: The priority queue is now empty. Planning terminates after exactly 3 steps.

Result Comparison

In just 3 focused planning steps, the goal reward rippled completely across the 3-state chain:

  • Q(S3,right)=10.0000Q(S_3, \text{right}) = 10.0000
  • Q(S2,right)=9.0000Q(S_2, \text{right}) = 9.0000
  • Q(S1,right)=8.1000Q(S_1, \text{right}) = 8.1000

Under uniform random Dyna-Q with 100 transitions in memory, achieving this backward cascade would have required hundreds of exploratory iterations.

Code

Below is a complete, type-hinted Python implementation of Prioritized Sweeping utilizing a priority queue built on Python's standard heapq module:

import heapqfrom typing import Dict, List, Optional, Set, Tuple

class PriorityQueue:    """Max-priority queue wrapper using heapq supporting priority promotion."""
    def __init__(self) -> None:        self._heap: List[Tuple[float, int, Optional[Tuple[str, str]]]] = []        self._entry_finder: Dict[Tuple[str, str], List] = {}        self._counter: int = 0
    def push_or_update(self, item: Tuple[str, str], priority: float) -> None:        """Insert or increase priority of an existing state-action pair."""        if item in self._entry_finder:            existing_entry = self._entry_finder[item]            existing_priority = -existing_entry[0]            # In prioritized sweeping, retain the higher priority            if priority <= existing_priority:                return            # Invalidate older entry in heap            existing_entry[-1] = None
        self._counter += 1        # Store negative priority because heapq implements a min-heap        entry = [-priority, self._counter, item]        self._entry_finder[item] = entry        heapq.heappush(self._heap, entry)
    def pop(self) -> Tuple[Tuple[str, str], float]:        """Pop and return (item, priority) with the highest priority."""        while self._heap:            neg_pri, _, item = heapq.heappop(self._heap)            if item is not None:                del self._entry_finder[item]                return item, -neg_pri        raise IndexError("Cannot pop from an empty PriorityQueue")
    def __bool__(self) -> bool:        return bool(self._entry_finder)
    def __len__(self) -> int:        return len(self._entry_finder)

class PrioritizedSweeping:    """Model-based Prioritized Sweeping algorithm."""
    def __init__(        self,        states: List[str],        actions: List[str],        gamma: float = 0.9,        alpha: float = 1.0,        theta: float = 0.01,        max_planning_steps: int = 10,    ) -> None:        self.states = states        self.actions = actions        self.gamma = gamma        self.alpha = alpha        self.theta = theta        self.max_planning_steps = max_planning_steps
        # Action-value table        self.q: Dict[Tuple[str, str], float] = {            (s, a): 0.0 for s in states for a in actions        }
        # Transition model: (state, action) -> (reward, next_state)        self.model: Dict[Tuple[str, str], Tuple[float, str]] = {}
        # Reverse predecessor index: state -> set of (pred_state, pred_action)        self.predecessors: Dict[str, Set[Tuple[str, str]]] = {            s: set() for s in states        }
        self.pqueue = PriorityQueue()
    def get_max_q(self, state: str) -> float:        """Return max_a Q(state, a) or 0.0 if state is terminal."""        q_vals = [self.q[(state, a)] for a in self.actions if (state, a) in self.q]        return max(q_vals) if q_vals else 0.0
    def step(        self,        state: str,        action: str,        reward: float,        next_state: str,        is_terminal: bool = False,    ) -> List[Tuple[Tuple[str, str], float, float]]:        """Process an experience transition and perform prioritized planning.
        Returns:            List of planning steps: [((state, action), priority, new_q_value), ...]        """        # 1. Update learned model and predecessor graph        self.model[(state, action)] = (reward, next_state)        if next_state not in self.predecessors:            self.predecessors[next_state] = set()        self.predecessors[next_state].add((state, action))
        # 2. Compute Bellman error for experienced transition        next_max = 0.0 if is_terminal else self.get_max_q(next_state)        target = reward + self.gamma * next_max        priority = abs(target - self.q[(state, action)])
        # 3. Insert into queue if priority exceeds threshold        if priority > self.theta:            self.pqueue.push_or_update((state, action), priority)
        # 4. Backward planning ripple loop        planning_trace: List[Tuple[Tuple[str, str], float, float]] = []        steps_executed = 0
        while self.pqueue and steps_executed < self.max_planning_steps:            steps_executed += 1            (s, a), popped_p = self.pqueue.pop()
            r, s_next = self.model[(s, a)]            s_next_max = 0.0 if (s_next == "S4" or is_terminal) else self.get_max_q(s_next)            td_target = r + self.gamma * s_next_max            self.q[(s, a)] += self.alpha * (td_target - self.q[(s, a)])            planning_trace.append(((s, a), popped_p, self.q[(s, a)]))
            # Backward ripple: inspect all predecessors that lead into s            for s_bar, a_bar in self.predecessors.get(s, set()):                r_bar, _ = self.model[(s_bar, a_bar)]                pred_target = r_bar + self.gamma * self.get_max_q(s)                pred_p = abs(pred_target - self.q[(s_bar, a_bar)])                if pred_p > self.theta:                    self.pqueue.push_or_update((s_bar, a_bar), pred_p)
        return planning_trace

def demonstrate_prioritized_sweeping() -> None:    states = ["S1", "S2", "S3", "S4"]    actions = ["right"]    ps = PrioritizedSweeping(        states, actions, gamma=0.9, alpha=1.0, theta=0.01, max_planning_steps=10    )
    # Pre-populate model with known transitions leading toward goal    ps.model[("S1", "right")] = (0.0, "S2")    ps.predecessors["S2"].add(("S1", "right"))    ps.model[("S2", "right")] = (0.0, "S3")    ps.predecessors["S3"].add(("S2", "right"))
    print("--- Experiencing Goal Discovery: S3 -> S4 with R=+10.0 ---")    trace = ps.step("S3", "right", 10.0, "S4", is_terminal=True)    for step_num, ((s, a), p, new_val) in enumerate(trace, 1):        print(            f"Planning Step {step_num}: Popped ({s}, {a}) with priority {p:.4f} "            f"-> Q updated to {new_val:.4f}"        )
    print("\n--- Final Q-Values after Backward Ripple ---")    for s in ["S1", "S2", "S3"]:        print(f"Q({s}, right) = {ps.q[(s, 'right')]:.4f}")
    assert abs(ps.q[("S3", "right")] - 10.0) < 1e-4    assert abs(ps.q[("S2", "right")] - 9.0) < 1e-4    assert abs(ps.q[("S1", "right")] - 8.1) < 1e-4

if __name__ == "__main__":    demonstrate_prioritized_sweeping()
# -> Expected output:# -> --- Experiencing Goal Discovery: S3 -> S4 with R=+10.0 ---# -> Planning Step 1: Popped (S3, right) with priority 10.0000 -> Q updated to 10.0000# -> Planning Step 2: Popped (S2, right) with priority 9.0000 -> Q updated to 9.0000# -> Planning Step 3: Popped (S1, right) with priority 8.1000 -> Q updated to 8.1000# -> # -> --- Final Q-Values after Backward Ripple ---# -> Q(S1, right) = 8.1000# -> Q(S2, right) = 9.0000# -> Q(S3, right) = 10.0000

Watch Out For

The Threshold Tuning Dilemma: Queue Explosion vs. Frozen Planning

The priority threshold θ\theta dictates the boundary between urgent updates and ignorable noise. Choosing an inappropriate θ\theta causes severe systemic failures:

The Symptom:

  1. Queue Thrashing (θ\theta too small): In stochastic environments or dense transition graphs, setting θ≈0\theta \approx 0 causes thousands of minuscule numerical adjustments to enter the heap. Heap management overhead (O(log⁡∣Q∣)\mathcal{O}(\log |Q|) per insertion) quickly dominates total wall-clock runtime, consuming more CPU cycles than environmental interaction itself.
  2. Frozen Propagation (θ\theta too large): If θ\theta is set too high relative to reward magnitudes, the backward ripple wave prematurely attenuates. Because predecessor errors decay geometrically by γ\gamma (Pˉ∝γkP\bar{P} \propto \gamma^k P), a high threshold stops the value wave after just one or two steps, leaving distant start states completely unaware of discovered goals.

The Fix:

  • Scale θ\theta proportionally to the environment's reward magnitude and discount factor: a practical rule of thumb is θ≈10−3×∣Rmax⁡−Rmin⁡∣\theta \approx 10^{-3} \times |R_{\max} - R_{\min}|.
  • Always enforce an explicit cap on planning steps per real interaction (max_planning_steps = n). This bounds computation per environmental step regardless of queue size.
  • In stochastic environments, maintain expected transition probability models ∑s′p(s′∣s,a)\sum_{s'} p(s' \mid s, a) rather than deterministic overwrites to prevent stochastic transition jitter from triggering phantom priority spikes.

The Quick Version

  • Urgency-Driven Planning: Prioritized sweeping replaces Dyna-Q's blind uniform sampling with a priority queue ordered by absolute Bellman error P=∣R+γmax⁡Q(S′)−Q(S,A)∣P = |R + \gamma \max Q(S') - Q(S, A)|.
  • Backward Value Propagation: Whenever a high-priority state is updated, the algorithm inspects all predecessor transitions that lead into it, queuing them to receive the newly computed value signal.
  • Exponentially Faster Learning: High-magnitude discoveries ripple backward toward the starting state in a direct cascade, reducing the number of planning steps required for goal information to propagate by orders of magnitude.
  • Threshold Controlled: The priority threshold θ\theta acts as a filter, pruning quiescent transitions from the queue to prevent wasted computation on states whose values have already converged.