Reinforcement Learning Lab
A reinforcement learning agent is never told the right answer: it acts, collects rewards or penalties, and learns a policy that maximizes the cumulative reward.
Based on Chapter 12, “Reinforcement learning,” in Alaa Khamis, Optimization Algorithms: AI techniques for design, planning, and control problems, Manning Publications, 2024 (ISBN 978-1633438835).
A Q-learning agent learning its way to the treasure.
Reinforcement learning
An agent observes a state, takes an action from its policy π, collects a reward and moves on, learning by trial and error. In Markov decision processes (MDPs) actions change future states; in multi-armed bandits the agent only chooses among actions.
Section 12.1 explains the agent-environment loop (Figure 12.3) and maps the RL algorithm families in Figure 12.1.
Markov decision processes
Real commutes are uncertain, so the next state follows probabilities: an MDP. Switch between the deterministic and nondeterministic models, pick a mode of transport and take trips.
Section 12.1.1 contrasts the two commute models of Figure 12.2 and defines the expected return and discount factor γ.
Take the commute (Figure 12.2)
Blue: routes your choice can take. Red italics: transition probabilities. Gold: the current trip; edges thicken with use.
Reach the Treasure (Figure 12.4)
A stochastic policy moves the agent left or right between a fire pit (−10), the treasure (+10) and the exit (+4); its expected return weights each reward by its probability:
Play episodes or drag the policy sliders.
Section 12.1.2 computes this expected return by hand (Figure 12.4) and solves the game with value iteration.
Walk the policy tree
🔥 fire pit (−10, ends) · 💎 treasure (+10) · 🤖 agent · 🏁 exit (+4, ends). Each card is a state the agent can reach; p is the probability of reaching it under the current policy. obs = [cell, treasure collected]; policies are [left, right] probabilities.
Learning by trial and error: Q-learning
Q-learning learns the return Q(s, a) of each action by trial and error, with no model. Press Learn and watch the arrows settle; click cells to cycle empty, wall and fire pit.
Section 12.1.2 derives the Q-learning update and explains why it is off-policy.
A gridworld
Each move −0.1; treasure +10; fire pit −10Values, policies and actor-critic
Policy gradient methods make actions with a positive advantage  = Q(s, a) − V(s) more likely; in actor-critic methods such as A2C, a critic network estimates V(s) for the actor.
Sections 12.1.2 to 12.1.4 build up equations 12.1 to 12.4, compare model-based, model-free, on-policy and off-policy methods (Tables 12.1 and 12.2), and draw A2C in Figure 12.5.
Balancing CartPole with an actor-critic
Keep the pole upright (+1 per step). Play manually, then train an A2C agent and watch the return per episode climb.
Section 12.3 details CartPole’s states and stopping rules; Listings 12.2 and 12.3 train A2C and PPO with Stable-Baselines3.
CartPole
Keyboard in manual mode: ← and →Proximal policy optimization
PPO keeps each policy update close to the old policy. PPO-clip caps how far the probability ratio r(θ) = πθ(a|s) / πold(a|s) can move from 1:
LCLIP(θ) = Ê[min(r(θ) Â, clip(r(θ), 1 − ε, 1 + ε) Â)]
Drag r(θ) on the plots, or sweep it, to see where the objective goes flat.
Section 12.1.5 explains why big updates are risky and compares PPO-penalty (equation 12.5) with PPO-clip (Figure 12.6).
The clipped objective
Solid: LCLIP. Dashed gray: unclipped r(θ)Â. Gold arrow: the direction the update pushes r(θ) (none on a flat part).
The KL divergence measures how different two distributions are; it is not symmetric. Move the sliders to see it change.
Section 12.1.5 defines it and shows its role in PPO.
KL divergence between two Gaussians
Multi-armed bandits
Three slot machines, unknown payoffs, 90 pulls (Figure 12.7). Pick a strategy, press Play, then compare strategies over 2,000 runs by their regret.
Section 12.1.6 works out each strategy’s regret, shows the greedy trap and derives UCB (equation 12.7); Listing 12.1 codes all four.
Play 90 pulls
Curves: true payoffs (hidden from the player). Ticks: payoffs so far. Dashed gold: the estimate Q. Bars: share of the 90 pulls.
Contextual bandits: choosing a delivery truck
Each truck is an arm, and the route type is the context that changes which arm is best. Press Learn to watch an ε-greedy learner pick a truck per route.
Section 12.5 builds this reward model and solves it with Vowpal Wabbit (Listing 12.5).
Learn a truck per route
Reinforcement learning for optimization
Framed as an MDP, a combinatorial problem can be solved by a learned policy.
Sections 12.2 and 12.4 survey routing, dispatch and traffic applications, and Listing 12.4 coordinates a mobile network with PPO.
Python in Google Colab
Open the book’s Chapter 12 notebooks in Colab, or the companion notebook with the from-scratch code below (Python and NumPy only).
Notebooks
From-scratch implementations
# MDPs, value iteration and Q-learning (Sections 12.1.1 and 12.1.2) import random # 1. The commute of Figure 12.2: expected travel time in minutes under uncertainty alternative = 180 # assumed time if the train never comes and you look for another way car = 0.3 * 50 + 0.5 * 67 + 0.2 * 120 train = 0.6 * 108 + 0.4 * (0.9 * 145 + 0.1 * alternative) bike = 196 print(f"expected minutes: car {car:.1f}, train {train:.1f}, bike {bike}") # 2. Reach the Treasure (Figure 12.4): the expected return of the book's single trajectory tree R = 0.4 * 10 + 0.6 * 4 + 0.4 * 0.2 * -10 + 0.4 * 0.8 * 0 + 0.4 * 0.8 * 0.9 * 0 + 0.4 * 0.8 * 0.1 * 4 print(f"expected return of the trajectory tree: {R:.3f}") # 5.728 # 3. The same game as an MDP. Cells 0 (fire pit, -10, terminal), 1 (treasure, +10 once), 2 (start), 3 (exit, +4, terminal). # A state is (cell, treasure still there?). Actions: 0 = left, 1 = right. def step(state, action): cell, treasure = state cell += -1 if action == 0 else 1 if cell == 0: return (cell, treasure), -10, True if cell == 3: return (cell, treasure), 4, True if cell == 1 and treasure: return (cell, False), 10, False return (cell, treasure), 0, False STATES = [(c, t) for c in (1, 2) for t in (True, False)] def value_iteration(gamma=0.9, tol=1e-9): V = {s: 0.0 for s in STATES} while True: delta = 0 for s in STATES: q = [] for a in (0, 1): s2, r, done = step(s, a) q.append(r + (0 if done else gamma * V[s2])) delta = max(delta, abs(max(q) - V[s])); V[s] = max(q) if delta < tol: return V def q_learning(episodes=2000, alpha=0.1, gamma=0.9, eps=0.2, seed=0): rng = random.Random(seed) Q = {(s, a): 0.0 for s in STATES for a in (0, 1)} for _ in range(episodes): s, done = (2, True), False while not done: a = rng.randrange(2) if rng.random() < eps else max((0, 1), key=lambda b: Q[(s, b)]) s2, r, done = step(s, a) target = r if done else r + gamma * max(Q[(s2, 0)], Q[(s2, 1)]) Q[(s, a)] += alpha * (target - Q[(s, a)]) # temporal-difference update s = s2 return Q V = value_iteration() Q = q_learning() print("state (cell, treasure) V* Q(left) Q(right) greedy action") for s in STATES: if s == (1, True): continue # unreachable: stepping onto cell 1 collects the treasure print(f"{str(s):24s} {V[s]:6.2f} {Q[(s, 0)]:6.2f} {Q[(s, 1)]:6.2f} {'left' if Q[(s, 0)] > Q[(s, 1)] else 'right'}") # optimal plan: left to the treasure (+10), then right twice to the exit (+4): 10 + 0.9 * 0 + 0.81 * 4 = 13.24 # 4. Q-learning in a gridworld: reach the treasure (+10), avoid the fire pits (-10); every move costs 0.1 GRID = ["....#T", ".F.#..", "...#F.", ".#....", ".F.F#.", "S....."] MOVES = [(-1, 0), (0, 1), (1, 0), (0, -1)] # up, right, down, left def grid_step(pos, a): r, c = pos[0] + MOVES[a][0], pos[1] + MOVES[a][1] if not (0 <= r < 6 and 0 <= c < 6) or GRID[r][c] == "#": r, c = pos # bump into the edge or a wall: stay put cell = GRID[r][c] if cell == "T": return (r, c), 10, True if cell == "F": return (r, c), -10, True return (r, c), -0.1, False def grid_q_learning(episodes=500, alpha=0.5, gamma=0.95, eps=0.1, seed=0): rng = random.Random(seed) Q = {} lengths = [] for _ in range(episodes): s, done, steps = (5, 0), False, 0 while not done and steps < 200: q = [Q.get((s, a), 0.0) for a in range(4)] a = rng.randrange(4) if rng.random() < eps else max(range(4), key=lambda b: (q[b], rng.random())) s2, r, done = grid_step(s, a) target = r if done else r + gamma * max(Q.get((s2, b), 0.0) for b in range(4)) Q[(s, a)] = q[a] + alpha * (target - q[a]) s, steps = s2, steps + 1 lengths.append(steps) return Q, lengths Q, lengths = grid_q_learning() print("gridworld: steps in the first 5 episodes", lengths[:5], "and the last 5", lengths[-5:]) arrows = "^>v<" for r in range(6): print(" " + " ".join(GRID[r][c] if GRID[r][c] in "#TF" else arrows[max(range(4), key=lambda a: Q.get(((r, c), a), 0.0))] for c in range(6)))
# Multi-armed bandits: three slot machines and 90 pulls (Section 12.1.6, Listing 12.1) import math import random MEANS, SDS, T = [8, 10, 5], [0.5, 0.7, 0.45], 90 BEST = max(MEANS) def pull(rng, k): return rng.gauss(MEANS[k], SDS[k]) def play(strategy, rng, eps=0.1, c=2.0): """Return the average regret per pull. Greedy, epsilon-greedy and UCB use only the means they have estimated.""" n, s, regret = [0] * 3, [0.0] * 3, 0.0 for t in range(T): if strategy == "explore": k = rng.randrange(3) elif t < 3: k = t # try each machine once elif strategy == "greedy": k = max(range(3), key=lambda i: s[i] / n[i]) elif strategy == "epsilon": k = rng.randrange(3) if rng.random() < eps else max(range(3), key=lambda i: s[i] / n[i]) else: # UCB, equation 12.7 k = max(range(3), key=lambda i: s[i] / n[i] + c * math.sqrt(math.log(t) / n[i])) r = pull(rng, k) n[k] += 1; s[k] += r regret += BEST - r return regret / T rng = random.Random(1) print("explore-only, expected: (900 - 690) / 90 =", round((900 - 690) / 90, 3)) for name in ("explore", "greedy", "epsilon", "ucb"): runs = [play(name, rng) for _ in range(2000)] print(f"{name:8s} average regret per pull over 2,000 runs: {sum(runs) / len(runs):.3f}") # The greedy trap of the book's example: first payoffs 7, 6 and 3 make machine 1 look best forever n, s = [1, 1, 1], [7.0, 6.0, 3.0] picks = [0, 0, 0] for t in range(3, T): k = max(range(3), key=lambda i: s[i] / n[i]) r = pull(rng, k); n[k] += 1; s[k] += r; picks[k] += 1 print("greedy after first payoffs (7, 6, 3): pulls per machine", picks)
# Balancing CartPole with an advantage actor-critic (A2C) written from scratch (Section 12.3, Listing 12.2) import math import numpy as np # CartPole-v1 dynamics (the same equations and constants as Gym's implementation) G, MC, MP, L, F, TAU = 9.8, 1.0, 0.1, 0.5, 10.0, 0.02 def step(s, a): x, v, th, w = s force = F if a == 1 else -F c, sn = math.cos(th), math.sin(th) temp = (force + MP * L * w * w * sn) / (MC + MP) acc_th = (G * sn - c * temp) / (L * (4 / 3 - MP * c * c / (MC + MP))) acc_x = temp - MP * L * acc_th * c / (MC + MP) s = (x + TAU * v, v + TAU * acc_x, th + TAU * w, w + TAU * acc_th) done = abs(s[0]) > 2.4 or abs(s[2]) > 12 * math.pi / 180 # cart off the track or pole past 12 degrees return s, done class MLP: """A small two-layer network trained with Adam; the actor ends in a softmax, the critic in one linear unit.""" def __init__(self, n_in, n_hidden, n_out, rng, lr): self.W1 = rng.normal(0, 1 / math.sqrt(n_in), (n_in, n_hidden)); self.b1 = np.zeros(n_hidden) self.W2 = rng.normal(0, 1 / math.sqrt(n_hidden), (n_hidden, n_out)); self.b2 = np.zeros(n_out) self.lr, self.t = lr, 0 self.m = [np.zeros_like(p) for p in self.params()]; self.v = [np.zeros_like(p) for p in self.params()] def params(self): return [self.W1, self.b1, self.W2, self.b2] def forward(self, x): self.x = x; self.h = np.maximum(0, x @ self.W1 + self.b1) return self.h @ self.W2 + self.b2 def backward(self, dout): """Gradient-descent step on a loss whose gradient with respect to the output is dout.""" dh = (self.W2 @ dout) * (self.h > 0) grads = [np.outer(self.x, dh), dh, np.outer(self.h, dout), dout] self.t += 1 for p, g, m, v in zip(self.params(), grads, self.m, self.v): m[:] = 0.9 * m + 0.1 * g; v[:] = 0.999 * v + 0.001 * g * g p -= self.lr * (m / (1 - 0.9 ** self.t)) / (np.sqrt(v / (1 - 0.999 ** self.t)) + 1e-8) def train(episodes=500, gamma=0.99, seed=0, max_steps=500): rng = np.random.default_rng(seed) actor, critic = MLP(4, 32, 2, rng, 1e-3), MLP(4, 32, 1, rng, 5e-3) returns = [] for ep in range(episodes): s, total = tuple(rng.uniform(-0.05, 0.05, 4)), 0 for _ in range(max_steps): x = np.array(s) logits = actor.forward(x); p = np.exp(logits - logits.max()); p /= p.sum() a = rng.choice(2, p=p) s2, done = step(s, a) r = 1.0 v = critic.forward(x)[0] v2 = 0.0 if done else critic.forward(np.array(s2))[0] advantage = r + gamma * v2 - v # A = r + gamma V(s') - V(s) critic.forward(x); critic.backward(np.array([-advantage])) # minimize advantage^2 actor.forward(x); actor.backward((p - np.eye(2)[a]) * advantage) # minimize -log pi(a|s) * A total += r; s = s2 if done: break returns.append(total) if (ep + 1) % 50 == 0: print(f"episodes {ep - 48:3d}-{ep + 1:3d}: mean return {np.mean(returns[-50:]):6.1f}") return returns returns = train() print("best episode:", max(returns))
# PPO's clipped surrogate objective and the KL divergence (Section 12.1.5) import math def l_clip(r, adv, eps=0.2): """min(r A, clip(r, 1 - eps, 1 + eps) A): equation 12.6 for one sample.""" return min(r * adv, max(1 - eps, min(1 + eps, r)) * adv) print(" ratio r A = +1 A = -1") for r in (0.5, 0.8, 0.9, 1.0, 1.1, 1.2, 1.5, 2.0): print(f" {r:4.1f} {l_clip(r, 1):6.2f} {l_clip(r, -1):6.2f}") # For A > 0 the objective stops growing once r > 1 + eps; for A < 0 it stops once r < 1 - eps. def kl_gauss(m1, s1, m2, s2): """KL(P || Q) for P = N(m1, s1^2) and Q = N(m2, s2^2).""" return math.log(s2 / s1) + (s1 ** 2 + (m1 - m2) ** 2) / (2 * s2 ** 2) - 0.5 print("KL(N(0,1) || N(0,1)) =", kl_gauss(0, 1, 0, 1)) print("KL(N(0,1) || N(1,1)) =", kl_gauss(0, 1, 1, 1)) print("KL(N(0,1) || N(0,2)) =", round(kl_gauss(0, 1, 0, 2), 4), " vs. KL(N(0,2) || N(0,1)) =", round(kl_gauss(0, 2, 0, 1), 4), "(not symmetric)")
# Contextual bandit for delivery truck selection (Section 12.5, Listing 12.5), with a tabular epsilon-greedy learner import random from itertools import product CONTEXTS = ["city", "interstate"] SIZES, ENGINES, TIRES = ["small", "medium", "large"], ["petrol", "diesel", "electric"], ["all_season", "snow", "performance", "all_terrain"] ARMS = list(product(range(3), range(3), range(4))) # 36 trucks def reward_book(ctx, a, rng): """Listing 12.5's reward: the context is passed in but not used.""" i, j, k = a return [0.8, 1.0, 0.9][i] * [0.7, 0.9, 1.0][j] * [0.9, 0.8, 1.0, 0.95][k] + rng.gauss(0, 0.05) def reward_contextual(ctx, a, rng): """A variant where the route matters: small electric trucks suit the city, large diesel ones the interstate.""" i, j, k = a size = {"city": [1.0, 0.85, 0.6], "interstate": [0.6, 0.85, 1.0]}[ctx][i] engine = {"city": [0.7, 0.8, 1.0], "interstate": [0.8, 1.0, 0.7]}[ctx][j] return size * engine * [0.9, 0.8, 1.0, 0.95][k] + rng.gauss(0, 0.05) def learn(reward, iters=2500, eps=0.2, seed=0): rng = random.Random(seed) n = {(c, a): 0 for c in CONTEXTS for a in ARMS} q = {(c, a): 0.0 for c in CONTEXTS for a in ARMS} for _ in range(iters): c = rng.choice(CONTEXTS) a = rng.choice(ARMS) if rng.random() < eps else max(ARMS, key=lambda b: q[(c, b)]) r = reward(c, a, rng) n[(c, a)] += 1; q[(c, a)] += (r - q[(c, a)]) / n[(c, a)] # running mean return {c: max(ARMS, key=lambda b: q[(c, b)]) for c in CONTEXTS} name = lambda a: f"{SIZES[a[0]]}, {ENGINES[a[1]]}, {TIRES[a[2]]}" for label, fn in (("book reward (ignores the route)", reward_book), ("route-dependent reward", reward_contextual)): best = learn(fn) print(label) for c in CONTEXTS: print(f" {c:10s} -> {name(best[c])}") print("best noise-free book reward: medium, electric, performance = 1.0; with a snow tire it is 0.8")
What to remember
MDPs and bandits
MDP actions change future states; bandits only trade off exploration and exploitation.
Values and advantages
The advantage tells a policy gradient which actions to reinforce.
Stable updates
PPO clips the probability ratio to keep updates small.
RL for optimization
Routing, dispatch and network coordination can be learned as policies.
References: R. S. Sutton and A. G. Barto, Reinforcement Learning: An Introduction, 2nd ed., 2018. J. Schulman et al., “Proximal policy optimization algorithms,” 2017. V. Mnih et al., “Asynchronous methods for deep reinforcement learning,” 2016. A. G. Barto, R. S. Sutton and C. W. Anderson, “Neuronlike adaptive elements that can solve difficult learning control problems” (CartPole), 1983. S. Schneider et al., “mobile-env: an open platform for reinforcement learning in wireless mobile networks,” 2022. A. Khamis, Optimization Algorithms: AI techniques for design, planning, and control problems, Manning, 2024, Chapter 12.
Check your understanding
Two quick questions on Chapter 12. Pick an answer to check it.
Q1. In PPO-clip, the advantage  of an action is positive and the probability ratio r(θ) has already risen above 1 + ε. What does the clipped objective LCLIP do?
Above 1 + ε the clipped term caps the objective at (1 + ε)Â, so the gradient is zero and the update has no incentive to move the policy further from the old one. A KL penalty belongs to the separate PPO-penalty variant.Q2. Why can the purely greedy (exploit-only) bandit strategy get stuck on a worse slot machine, while UCB does not?
After one unlucky payoff, a greedy player never pulls the best machine again. UCB plays argmax [Q(a) + c √(ln t / N(a))], so a machine pulled only a few times keeps a large bonus and gets retried.For more exercises with full solutions, see Appendix C, “Exercises and solutions,” in Optimization Algorithms. The coding solutions are also on GitHub (Chapter 12).