Prediction told us how good the random policy is. Nobody wants the random policy. This section turns Monte Carlo evaluation into a full control algorithm: one that starts knowing nothing about the GridWorld and ends up walking the shortest path to the goal.
The recipe is one you’ve already seen. Policy Improvement introduced generalized policy iteration (GPI): evaluate the current policy, act greedily with respect to the evaluation, repeat. DP ran that loop with exact, model-based evaluation. MC control runs the same loop with sampled episodes—and that swap forces two changes, which are the real content of this section: we must learn Q-values instead of V-values, and we must keep exploring.
Why Q, Not V
Suppose MC prediction hands you a perfect for the grid. Now, standing at (2,2), which action is best? To answer, you’d reason: “moving right leads to (2,3), moving down leads to (3,2)…”—but knowing where actions lead is exactly the model we don’t have. A value function over states is only actionable if you can look one step ahead.
Action values dissolve the problem. If you have for all four moves, the greedy choice is a table lookup: pick the largest. No lookahead, no model.
“Which action leads to the best state?”
Requires p(s’|s,a) to simulate each action’s consequences. Needs the model.
“Which action has the highest value?”
argmax over four numbers. Model-free. This is why every model-free control method in this book learns Q.
Estimating by Monte Carlo is the same averaging as before, just keyed by state–action pairs: average the returns that followed each visit to . (We defined in Action Value Functions.)
The greedy policy with respect to requires the model explicitly:
The greedy policy with respect to does not:
The cost is a larger table—one estimate per state–action pair rather than per state ( entries in our grid instead of 14)—and correspondingly more samples needed to fill it.
The Exploration Problem, Again
Averaging returns for only works if the agent actually tries action in state —repeatedly. But GPI pushes the policy toward greed, and a greedy policy tries exactly one action per state. The other three estimates go stale, frozen at whatever noise they last held. This is the exploration-exploitation tradeoff resurfacing—except now failing to explore doesn’t just cost reward, it silently corrupts the learning loop itself.
The classical fix, and the practical one:
Begin every episode at a randomly chosen state–action pair, then follow the greedy policy. Every pair gets sampled by construction.
Elegant in theory; usually impossible in practice.
Never let any action’s probability hit zero: act ε-greedily and keep exploring forever.
Works with whatever start states the world gives you. The standard choice.
Why Exploring Starts Is Impractical
Monte Carlo control with exploring starts (MC-ES) is historically important and worth knowing—Sutton & Barto use it to solve blackjack. But look at what it assumes:
So we take the ε-soft route: exploration lives inside the policy itself.
ε-Greedy Monte Carlo Control
The full algorithm is a loop with three moves per episode:
There is no separate “improvement step” in the code—the ε-greedy action selection simply reads the freshly updated Q on the next episode. Evaluation and improvement interleave at the finest possible grain, which is GPI in its loosest, most practical form: we don’t wait for the evaluation to be accurate before improving, we just keep both processes running against each other.
An ε-soft policy gives every action probability at least . The ε-greedy policy derived from is the member of that family closest to greedy:
- For the greedy action : probability
- For every other action: probability
The policy improvement theorem extends to this family: making the policy ε-greedy with respect to produces a policy at least as good as any ε-soft , and iterating converges to the best ε-soft policy (Sutton & Barto, Section 5.4). Not the optimal policy—the best policy that still explores. In practice one either accepts that gap, decays over time, or goes off-policy (next section).
For the update itself we use the incremental mean per pair, first-visit:
A subtlety worth knowing: because the policy changes between episodes, the returns being averaged were generated by different (older, worse) policies. Sample averages weight them all equally, which technically muddies the estimate; constant-α updates, which forget old data, are the usual remedy. On our small grid the sample-average version works fine.
Implementation
Complete ε-greedy MC control on the same GridWorld (the step function is unchanged from the prediction section):
import numpy as np
from collections import defaultdict
ROWS, COLS = 4, 4
WALL, GOAL, START = (1, 1), (3, 3), (0, 0)
ACTIONS = ['up', 'down', 'left', 'right']
MOVES = {'up': (-1, 0), 'down': (1, 0), 'left': (0, -1), 'right': (0, 1)}
GAMMA = 0.9
def step(state, action):
"""One environment step. Returns (next_state, reward, done)."""
dr, dc = MOVES[action]
nxt = (state[0] + dr, state[1] + dc)
if not (0 <= nxt[0] < ROWS and 0 <= nxt[1] < COLS) or nxt == WALL:
nxt = state # bounce off wall or edge, still pay the step cost
reward = -1.0 + (10.0 if nxt == GOAL else 0.0)
return nxt, reward, nxt == GOAL
def epsilon_greedy(Q, state, epsilon, rng):
"""Pick a random action with prob. epsilon, else the greedy one."""
if rng.random() < epsilon:
return ACTIONS[rng.integers(4)]
qs = [Q[(state, a)] for a in ACTIONS]
best = max(qs)
# break ties randomly so early learning doesn't lock onto one action
candidates = [a for a, q in zip(ACTIONS, qs) if q == best]
return candidates[rng.integers(len(candidates))]
def mc_control(num_episodes, epsilon=0.1, gamma=GAMMA, seed=0, max_steps=1000):
"""First-visit Monte Carlo control with an epsilon-greedy policy."""
rng = np.random.default_rng(seed)
Q = defaultdict(float) # action-value estimates
N = defaultdict(int) # first-visit counts per (state, action)
episode_returns = []
for _ in range(num_episodes):
# 1. Generate an episode with the CURRENT epsilon-greedy policy
state, trajectory, done, t = START, [], False, 0
while not done and t < max_steps:
action = epsilon_greedy(Q, state, epsilon, rng)
next_state, reward, done = step(state, action)
trajectory.append((state, action, reward))
state = next_state
t += 1
# 2. Walk backward, computing returns
G = 0.0
returns = []
for state, action, reward in reversed(trajectory):
G = reward + gamma * G
returns.append((state, action, G))
returns.reverse()
episode_returns.append(returns[0][2]) # return from the start state
# 3. First-visit update for each (state, action) pair
seen = set()
for state, action, G in returns:
if (state, action) not in seen:
seen.add((state, action))
N[(state, action)] += 1
Q[(state, action)] += (G - Q[(state, action)]) / N[(state, action)]
# (No explicit improvement step: epsilon_greedy reads the updated Q
# on the very next episode. Evaluation and improvement interleave.)
return Q, episode_returns
Q, episode_returns = mc_control(num_episodes=20_000)
ARROW = {'up': '^', 'down': 'v', 'left': '<', 'right': '>'}
print("Greedy policy after 20,000 episodes:")
for r in range(ROWS):
row = []
for c in range(COLS):
if (r, c) == WALL:
row.append('X')
elif (r, c) == GOAL:
row.append('G')
else:
row.append(ARROW[max(ACTIONS, key=lambda a: Q[((r, c), a)])])
print(' ' + ' '.join(row))
print("\nV(s) = max_a Q(s,a):")
for r in range(ROWS):
print(' '.join(
' WALL' if (r, c) == WALL else ' GOAL' if (r, c) == GOAL
else f'{max(Q[((r, c), a)] for a in ACTIONS):6.2f}' for c in range(COLS)))
print(f"\nMean return, first 100 episodes: {np.mean(episode_returns[:100]):.2f}")
print(f"Mean return, last 100 episodes: {np.mean(episode_returns[-100:]):.2f}")Output:
Greedy policy after 20,000 episodes:
> > v v
v X > v
> > > v
> ^ > G
V(s) = max_a Q(s,a):
0.62 1.93 3.37 5.01
1.66 WALL 4.97 6.87
3.17 4.85 6.90 9.00
1.86 2.56 9.00 GOAL
Mean return, first 100 episodes: 0.00
Mean return, last 100 episodes: 0.69Reading the Results Honestly
The learned policy reaches the goal from the start in the minimum six steps, and checking it against the DP solution confirms: the exact value of this greedy policy at the start state is 1.22, identical to . (It gets there fast, too—rerunning with only 100 episodes already yields a start-optimal greedy policy on this seed.) But the printout repays a closer look, because two things are “off” in instructive ways.
The values are lower than . Compare the learned with the true optimal values from value iteration:
| Cell | Learned max Q | True V* |
|---|---|---|
| (0,0) start | 0.62 | 1.22 |
| (2,0) | 3.17 | 3.85 |
| (2,3) | 9.00 | 9.00 |
The estimates aren’t wrong—they’re answering a different question. Q converged toward the value of the ε-greedy policy, which wastes 10% of its steps on random actions. Far from the goal there’s more future left to fumble, so the gap is larger there; on the final step into the goal ((2,3) and (3,2)), where no future remains, the values agree exactly at 9.00. This is the “best ε-soft policy” ceiling from the theory, visible in the numbers.
Rarely visited states keep bad actions. Look at the bottom-left corner of the policy: (3,1) points up—a detour, not the shortest path. Why? Episodes start at (0,0), and the improving policy routes them along the top and right; the bottom-left corner is only reached by exploration accidents. Its Q-estimates rest on a handful of noisy returns ((3,0) shows value 1.86 where says 5.39). MC control polishes the states it lives in and neglects the ones it doesn’t—harmless here (those states are off the optimal path from the start), but a real issue whenever “any state could be a start state.”
For MC-ES (exploring starts, greedy policy), whether the algorithm always converges to was open for decades—Sutton & Barto flag it as one of RL’s oldest open theoretical questions, with convergence proofs appearing only recently under specific conditions. The ε-soft version has the cleaner guarantee (best ε-soft policy), which is one more reason it’s the default.
Does Exploration Actually Matter?
It’s worth verifying that ε-greedy exploration is doing real work rather than serving superstition. One caveat first: with our default initialization and all returns at most 1.22, zero is an optimistic initial value—unvisited actions look attractive, so even ε = 0 explores by accident (the same optimistic-initialization effect as in the bandits chapter). To isolate the effect, we initialize Q pessimistically at −20 and compare pure greedy (ε = 0) against ε = 0.1, running 5,000 episodes for each of 30 seeds:
| Setting | Seeds reaching optimal policy | Mean start value | Worst start value |
|---|---|---|---|
| ε = 0 (greedy) | 17 / 30 | −0.73 | −10.00 |
| ε = 0.1 | 30 / 30 | 1.22 | 1.22 |
With no exploration, the greedy agent locks onto the first trajectory that beats −20 and stops looking: 13 of 30 runs end with a suboptimal policy, and the worst runs converge to a policy that never reaches the goal at all (value −10 = paying −1 forever, discounted). With ε = 0.1, every single run finds the optimal start-state policy. Exploration isn’t a hyperparameter nicety in MC control; it’s load-bearing.
The On-Policy Compromise
Notice the deal we struck: to keep learning about all actions, the agent must behave suboptimally forever—and what it learns is the value of that compromised, exploring policy, not of the greedy one we actually want to deploy. Learning about one policy while behaving with another is exactly the problem off-policy methods solve, and Monte Carlo has a classical tool for it: importance sampling. That’s Off-Policy Monte Carlo.