Learn a gridworld by trial and error

Watch Q-learning and SARSA learn a maze and a cliff walk from rewards alone, one move at a time, with no map of the world.

IntermediateExplained in Learning from experience: Q-learning

Learn a gridworld by trial and error

The agent (orange dot) knows nothing about the world. Each cell's four triangles show its estimate of each move's return; the dashed line is the route it currently thinks is best.

World
Method
0.30
0.50
Speed
Episodes0
Exploration now0.300
Average return, last 20 episodesn/a
Learned routegoes in circles (some moves still untried)

Challenges

0 of 3 complete

  1. In the maze, train until the learned route (the dashed line) reaches the +1 by the shortest path: 9 moves. (not yet)
  2. On the cliff, get Q-learning to learn the shortest route: 13 moves, right along the edge. (not yet)
  3. On the cliff, with exploration that does not fade (epsilon at least 0.05), get SARSA to learn a route that keeps at least one row away from the edge. (not yet)

Try this

  • Press Train at Watch speed. The first episodes wander; the first time the agent stumbles onto the +1, only the cell next to it learns anything.
  • Switch to Fast and watch the dashed route settle on the shortest path.
  • Switch to the cliff, train Q-learning to 500 episodes, then press Forget, switch to SARSA, and train again. Compare the routes and the return chart.
  • Set exploration to 0 on a fresh maze. Does the agent still explore?
Q-learning versus SARSA on a small cliffPython with NumPy
import numpy as np

# A small cliff: start S bottom left, goal G bottom right, cliff C between.
# Every move costs 1; stepping onto the cliff costs 100 more and ends the episode.
H, W = 3, 6
start, goal = (2, 0), (2, 5)
cliff = {(2, c) for c in range(1, 5)}
moves = [(-1, 0), (0, 1), (1, 0), (0, -1)]  # up, right, down, left

def step(s, a):
    r, c = s[0] + moves[a][0], s[1] + moves[a][1]
    nxt = (min(max(r, 0), H - 1), min(max(c, 0), W - 1))
    if nxt in cliff:
        return nxt, -101, True
    return nxt, -1, nxt == goal

def train(method, episodes=500, alpha=0.5, epsilon=0.1, seed=0):
    rng = np.random.default_rng(seed)
    Q = np.zeros((H, W, 4))
    def choose(s):  # epsilon-greedy, ties broken at random
        if rng.random() < epsilon:
            return int(rng.integers(4))
        q = Q[s]
        return int(rng.choice(np.flatnonzero(q == q.max())))
    for _ in range(episodes):
        s, a, done = start, choose(start), False
        while not done:
            s2, r, done = step(s, a)
            a2 = choose(s2)
            if done:
                target = r
            elif method == "q-learning":
                target = r + Q[s2].max()   # value of the best next action
            else:
                target = r + Q[s2][a2]     # value of the action it will take
            Q[s][a] += alpha * (target - Q[s][a])
            s, a = s2, a2
    return Q

def greedy_route(Q):
    s, route = start, [start]
    while s != goal and len(route) < 30:
        s, _, _ = step(s, int(np.argmax(Q[s])))
        route.append(s)
    return route

for method in ["q-learning", "sarsa"]:
    route = greedy_route(train(method))
    rows = sorted({r for r, _ in route})
    print(f"{method:10s}: {len(route) - 1} moves, uses rows {rows}")
q-learning: 7 moves, uses rows [1, 2]
sarsa     : 9 moves, uses rows [0, 1, 2]

Try "embedding", "softmax", "overfitting", or "backpropagation".