Monte Carlo Methods • Part 2 of 3
📝Draft

Monte Carlo Control

Exploring starts, ε-soft policies, and learning to act from episodes

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 VπV^\pi 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 Qπ(s,a)Q^\pi(s,a) for all four moves, the greedy choice is a table lookup: pick the largest. No lookahead, no model.

Greedy via V(s)

“Which action leads to the best state?”

Requires p(s’|s,a) to simulate each action’s consequences. Needs the model.

Greedy via Q(s,a)

“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 QπQ^\pi by Monte Carlo is the same averaging as before, just keyed by state–action pairs: average the returns that followed each visit to (s,a)(s, a). (We defined QπQ^\pi in Action Value Functions.)

Mathematical Details

The greedy policy with respect to VV requires the model explicitly:

π(s)=argmaxas,rp(s,rs,a)[r+γV(s)]\pi'(s) = \arg\max_a \sum_{s', r} p(s', r \mid s, a)\big[r + \gamma V(s')\big]

The greedy policy with respect to QQ does not:

π(s)=argmaxaQ(s,a)\pi'(s) = \arg\max_a Q(s, a)

The cost is a larger table—one estimate per state–action pair rather than per state (14×4=5614 \times 4 = 56 entries in our grid instead of 14)—and correspondingly more samples needed to fill it.

The Exploration Problem, Again

Averaging returns for (s,a)(s,a) only works if the agent actually tries action aa in state ss—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:

Exploring starts

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.

ε-soft policies

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:

1.
You must be able to start anywhere
A robot can’t teleport to an arbitrary state, and a recommender can’t conjure an arbitrary user session. Real systems start where they start. (Blackjack works because dealing yourself any hand is easy.)
2.
Every pair needs repeated visits
With millions of state–action pairs, uniformly seeding starts spreads samples impossibly thin—most pairs would be visited once or never.
3.
Some starts are forbidden
”Initialize the chemical plant in an arbitrary state, then take an arbitrary action” is not a sentence a safety engineer will sign off on.

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:

1️⃣
Run an episode with the current ε-greedy policy
2️⃣
Walk backward, computing returns
3️⃣
Update Q toward each pair’s return

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.

Mathematical Details

An ε-soft policy gives every action probability at least ϵnactions\frac{\epsilon}{n_{\text{actions}}}. The ε-greedy policy derived from QQ is the member of that family closest to greedy:

  • For the greedy action a=argmaxaQ(s,a)a^* = \arg\max_{a} Q(s, a): probability 1ϵ+ϵnactions1 - \epsilon + \frac{\epsilon}{n_{\text{actions}}}
  • For every other action: probability ϵnactions\frac{\epsilon}{n_{\text{actions}}}

The policy improvement theorem extends to this family: making the policy ε-greedy with respect to QπQ^\pi produces a policy at least as good as any ε-soft π\pi, 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 ϵ\epsilon over time, or goes off-policy (next section).

For the update itself we use the incremental mean per pair, first-visit:

N(s,a)N(s,a)+1,Q(s,a)Q(s,a)+1N(s,a)[GQ(s,a)]N(s,a) \leftarrow N(s,a) + 1, \qquad Q(s,a) \leftarrow Q(s,a) + \frac{1}{N(s,a)}\big[G - Q(s,a)\big]

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

</>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.69

Reading 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 VV^*. (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 VV^*. Compare the learned maxaQ(s,a)\max_a Q(s,a) with the true optimal values from value iteration:

CellLearned max QTrue V*
(0,0) start0.621.22
(2,0)3.173.85
(2,3)9.009.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 VV^* 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.”

ℹ️Note

For MC-ES (exploring starts, greedy policy), whether the algorithm always converges to π\pi^* 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 Q=0Q = 0 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:

SettingSeeds reaching optimal policyMean start valueWorst start value
ε = 0 (greedy)17 / 30−0.73−10.00
ε = 0.130 / 301.221.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.