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.
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:
- 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 .
- 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 steps requires on the order of 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:
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 , 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 :
- Forward Model: Stores deterministic or expected transitions:
- Predecessor Registry: Maps every successor state to the set of state-action pairs that have historically led to it:
2. The Priority Metric
The priority of any state-action pair is the absolute magnitude of its one-step Bellman error:
where:
- is the discount factor.
- is the current action-value estimate.
- and are the predicted reward and next state from .
If (where is a small positive threshold), is inserted into the priority queue with priority . If already resides in the queue, its priority is increased to .
3. The Planning Loop
After taking a real step, the agent performs up to planning iterations while the priority queue is non-empty:
- Pop the Top Priority: Remove with the largest priority from the queue:
- Execute Model Backup: Retrieve and update its action-value: (In deterministic environments, setting sets exactly to its updated target, reducing its Bellman error to zero).
- Propagate Backward to Predecessors: For every predecessor pair known to transition into :
- Retrieve its predicted reward from .
- Compute the anticipated Bellman error for the predecessor:
- If , insert or promote in the priority queue with priority .
4. Quiescence and Convergence
Unlike Dyna-Q, which always runs exactly 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 , 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:
System Parameters
- Discount factor:
- Step size: (full Bellman backup)
- Priority threshold:
- Planning capacity: steps
Model State
The agent has already traversed and with zero reward, establishing the model and predecessor registry:
- Initial values: .
- Terminal state has .
Event: Goal Discovery at
The agent executes , observes reward , and reaches terminal state .
- Model Update: and .
- Initial Bellman Error:
- Queue Insertion: Because , insert into with priority .
Planning Iteration 1
- Pop: Top element is with priority .
- Update :
- Backward Lookup: Predecessors of is .
- For , predicted reward is and next state is .
- Compute predecessor priority:
- Since , insert into with priority .
Planning Iteration 2
- Pop: Top element is with priority .
- Update :
- Backward Lookup: Predecessors of is .
- For , predicted reward is and next state is .
- Compute predecessor priority:
- Since , insert into with priority .
Planning Iteration 3
- Pop: Top element is with priority .
- Update :
- Backward Lookup: has no predecessors ().
- 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:
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.0000Watch Out For
The Threshold Tuning Dilemma: Queue Explosion vs. Frozen Planning
The priority threshold dictates the boundary between urgent updates and ignorable noise. Choosing an inappropriate causes severe systemic failures:
The Symptom:
- Queue Thrashing ( too small): In stochastic environments or dense transition graphs, setting causes thousands of minuscule numerical adjustments to enter the heap. Heap management overhead ( per insertion) quickly dominates total wall-clock runtime, consuming more CPU cycles than environmental interaction itself.
- Frozen Propagation ( too large): If is set too high relative to reward magnitudes, the backward ripple wave prematurely attenuates. Because predecessor errors decay geometrically by (), 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 proportionally to the environment's reward magnitude and discount factor: a practical rule of thumb is .
- 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 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 .
- 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 acts as a filter, pruning quiescent transitions from the queue to prevent wasted computation on states whose values have already converged.