Python Reference
A practical programming guide for the reinforcement learning labs
Python Reference
This page is a practical Python reference for The Mathematics of Reinforcement Learning. It summarizes the coding patterns used throughout the computer labs.
Purpose
The reinforcement-learning labs use Python to make the mathematics concrete. This reference collects common patterns that appear across the notebooks:
- arrays and tables,
- random simulation,
- policies,
- value functions,
- Q-functions,
- training loops,
- plotting,
- diagnostics,
- reproducibility,
- debugging.
The goal is not to teach all of Python. The goal is to provide enough Python to read, run, modify, and extend the labs.
Repository
The book repository is:
https://github.com/wanghemath/Book-MathRL
The labs are stored in:
labs/
A typical lab notebook is:
labs/chapter-01-lab.ipynb
A typical Colab URL is:
https://colab.research.google.com/github/wanghemath/Book-MathRL/blob/main/labs/chapter-01-lab.ipynb
Standard imports
Most labs use the following imports:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import randomThese libraries are enough for most tabular and small-scale reinforcement-learning experiments.
| Library | Main use |
|---|---|
numpy |
arrays, random numbers, vectorized computation |
pandas |
tables and experiment summaries |
matplotlib |
plots and visualizations |
random |
simple Python random choices |
Reproducibility
Set random seeds so that experiments can be repeated.
def set_seed(seed=0):
np.random.seed(seed)
random.seed(seed)
set_seed(42)In reinforcement learning, results may change across random seeds because of:
- random initialization,
- random actions,
- stochastic transitions,
- random datasets,
- minibatch sampling.
For final projects, it is often better to run several seeds and report averages.
Basic NumPy arrays
Create arrays
x = np.array([1, 2, 3])
zeros = np.zeros(5)
ones = np.ones(5)
matrix = np.zeros((3, 4))Shape
Q = np.zeros((10, 4))
print(Q.shape)Output:
(10, 4)
For a tabular Q-function:
Q.shape == (number_of_states, number_of_actions)Indexing
s = 3
a = 2
Q[s, a] = 1.5
value = Q[s, a]Row operations
best_action = np.argmax(Q[s])
best_value = np.max(Q[s])These appear frequently in greedy policy extraction.
Random choices
Random integer
a = np.random.randint(4)This samples an integer from:
0, 1, 2, 3
Random action from probabilities
probs = np.array([0.1, 0.2, 0.6, 0.1])
a = np.random.choice(4, p=probs)Shuffle data
np.random.shuffle(transitions)This is useful in offline reinforcement learning and fitted Q iteration.
Tables with pandas
Pandas is useful for displaying trajectories, summaries, and experiment results.
rows = []
rows.append({
"episode": 0,
"return": 1.2,
"length": 15,
"success": True
})
df = pd.DataFrame(rows)
dfSummary table
summary = pd.DataFrame([
{"method": "Q-learning", "success rate": 0.82, "mean return": 0.51},
{"method": "Dyna-Q", "success rate": 0.91, "mean return": 0.63},
])
summaryGroup by
df.groupby("method")["return"].mean()This is useful when comparing random seeds or algorithms.
Plotting with matplotlib
Basic line plot
plt.figure(figsize=(7, 4))
plt.plot(returns)
plt.xlabel("episode")
plt.ylabel("return")
plt.title("Learning curve")
plt.grid(True, alpha=0.3)
plt.show()Plot several curves
plt.figure(figsize=(7, 4))
plt.plot(q_learning_returns, label="Q-learning")
plt.plot(dyna_returns, label="Dyna-Q")
plt.xlabel("episode")
plt.ylabel("return")
plt.title("Comparison")
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()Bar chart
methods = ["Q-learning", "Dyna-Q", "Offline FQI"]
success_rates = [0.76, 0.88, 0.65]
plt.figure(figsize=(7, 4))
plt.bar(methods, success_rates)
plt.ylabel("success rate")
plt.title("Policy comparison")
plt.grid(True, axis="y", alpha=0.3)
plt.show()Heatmap
values = np.arange(16).reshape(4, 4)
plt.figure(figsize=(5, 4))
plt.imshow(values)
plt.colorbar(label="value")
plt.title("Value heatmap")
plt.show()Heatmaps are useful for value functions and state-visitation counts.
Rolling averages
RL learning curves are noisy. Rolling averages make trends easier to see.
def rolling_mean(x, window=20):
x = np.asarray(x, dtype=float)
if len(x) < window:
return x
return np.convolve(x, np.ones(window) / window, mode="valid")Use it like this:
plt.figure(figsize=(7, 4))
plt.plot(rolling_mean(returns, window=20))
plt.xlabel("episode")
plt.ylabel("20-episode rolling return")
plt.title("Smoothed learning curve")
plt.grid(True, alpha=0.3)
plt.show()Grid-world state indexing
Many labs use grid-world environments.
A two-dimensional position:
(row, col)
is converted to a one-dimensional state index:
state = row * n_cols + colThe inverse operation is:
row, col = divmod(state, n_cols)Example:
rows = 4
cols = 5
def to_state(row, col):
return row * cols + col
def to_pos(state):
return divmod(state, cols)
s = to_state(2, 3)
print(s)
print(to_pos(s))Minimal environment interface
Most labs use small custom environments with this interface:
s = env.reset()
sp, r, done, info = env.step(a)The common variables are:
| Variable | Meaning |
|---|---|
s |
current state |
a |
action |
sp |
next state |
r |
reward |
done |
whether the episode ended |
info |
optional diagnostic dictionary |
A minimal episode loop:
s = env.reset()
done = False
total_reward = 0.0
while not done:
a = np.random.randint(env.n_actions)
sp, r, done, info = env.step(a)
total_reward += r
s = spPolicy representations
A policy tells the agent how to choose actions.
Deterministic policy
A deterministic policy can be stored as an integer array:
policy = np.zeros(env.n_states, dtype=int)
policy[s] = 1Then action selection is:
a = int(policy[s])Stochastic policy
A stochastic policy can be stored as a matrix:
policy_probs = np.ones((env.n_states, env.n_actions)) / env.n_actionsThen action selection is:
a = np.random.choice(env.n_actions, p=policy_probs[s])Greedy policy from Q
policy = np.argmax(Q, axis=1)For terminal states, it is common to set the action to zero:
for s in terminal_states:
policy[s] = 0Epsilon-greedy action selection
Epsilon-greedy exploration is used in many labs.
def epsilon_greedy(Q, state, epsilon):
if np.random.rand() < epsilon:
return np.random.randint(Q.shape[1])
return int(np.argmax(Q[state]))Interpretation:
- with probability
epsilon, explore randomly; - with probability
1 - epsilon, exploit the current best action.
A decaying epsilon schedule:
epsilon = eps_end + (eps_start - eps_end) * np.exp(-episode / decay)Example:
eps_start = 1.0
eps_end = 0.05
decay = 300
epsilon = eps_end + (eps_start - eps_end) * np.exp(-episode / decay)Value functions
A tabular value function is a vector:
V = np.zeros(env.n_states)The value of state s is:
V[s]A Bellman-style update:
V[s] += alpha * (target - V[s])For TD(0), the target is:
target = r + gamma * V[sp]if the next state is nonterminal.
If the transition ended the episode:
target = rQ-functions
A tabular Q-function is a matrix:
Q = np.zeros((env.n_states, env.n_actions))The value of state-action pair (s,a) is:
Q[s, a]The greedy action is:
a_star = np.argmax(Q[s])The greedy value is:
max_q = np.max(Q[s])Q-learning update
The Q-learning target is:
target = r
if not done:
target += gamma * np.max(Q[sp])The update is:
td_error = target - Q[s, a]
Q[s, a] += alpha * td_errorAs a function:
def q_learning_update(Q, s, a, r, sp, done, alpha=0.1, gamma=0.95):
target = r
if not done:
target += gamma * np.max(Q[sp])
td_error = target - Q[s, a]
Q[s, a] += alpha * td_error
return td_errorMathematically:
\[ Q(s,a) \leftarrow Q(s,a) + \alpha \left[ r+\gamma\max_b Q(s',b)-Q(s,a) \right]. \]
SARSA update
SARSA uses the next action actually chosen by the behavior policy.
target = r
if not done:
target += gamma * Q[sp, ap]
td_error = target - Q[s, a]
Q[s, a] += alpha * td_errorMathematically:
\[ Q(s,a) \leftarrow Q(s,a) + \alpha \left[ r+\gamma Q(s',a')-Q(s,a) \right]. \]
Expected SARSA update
Expected SARSA uses the expected next Q-value under the policy.
expected_next = np.sum(policy_probs[sp] * Q[sp])
target = r
if not done:
target += gamma * expected_next
Q[s, a] += alpha * (target - Q[s, a])Monte Carlo returns
The discounted return from time t is:
\[ G_t = R_{t+1} + \gamma R_{t+2} + \gamma^2 R_{t+3}+\cdots. \]
Code:
def discounted_returns(rewards, gamma=0.95):
returns = np.zeros(len(rewards))
G = 0.0
for t in reversed(range(len(rewards))):
G = rewards[t] + gamma * G
returns[t] = G
return returnsExample:
rewards = [0, 0, 1]
discounted_returns(rewards, gamma=0.9)Policy gradient basics
For a softmax policy:
def softmax(logits):
z = logits - np.max(logits)
exp_z = np.exp(z)
return exp_z / np.sum(exp_z)If theta[s, a] stores logits for each state and action:
probs = softmax(theta[s])
a = np.random.choice(env.n_actions, p=probs)The REINFORCE update is based on:
\[ \nabla_\theta J(\theta) = E[ G_t\nabla_\theta\log\pi_\theta(A_t\mid S_t) ]. \]
For a tabular softmax policy, the gradient of the log probability is:
grad = -probs
grad[a] += 1.0Then update:
theta[s] += alpha * G * gradAdvantage estimates
An advantage compares an action to the state baseline:
\[ A(s,a)=Q(s,a)-V(s). \]
In actor-critic methods, the TD error often plays the role of advantage:
\[ \delta_t = R_{t+1} + \gamma V(S_{t+1}) - V(S_t). \]
Code:
td_error = r + gamma * V[sp] * (1 - done) - V[s]Then:
V[s] += alpha_v * td_error
theta[s] += alpha_pi * td_error * grad_log_probLinear function approximation
A linear value function has the form:
\[ V(s;w)=\phi(s)^T w. \]
Code:
def value(phi_s, w):
return phi_s @ wPrediction:
v_s = features[s] @ wUpdate:
target = r + gamma * (features[sp] @ w)
error = target - features[s] @ w
w += alpha * error * features[s]Feature matrices
A feature matrix stores one row per state:
Phi = np.zeros((n_states, n_features))The feature vector of state s is:
phi_s = Phi[s]Linear prediction for all states:
V = Phi @ wLeast-squares solution:
w_hat = np.linalg.solve(Phi.T @ Phi, Phi.T @ y)Safer version with ridge regularization:
ridge = 1e-6
w_hat = np.linalg.solve(Phi.T @ Phi + ridge * np.eye(Phi.shape[1]), Phi.T @ y)Replay buffers
Deep Q-learning uses replay buffers to reuse transitions.
A simple replay buffer:
class ReplayBuffer:
def __init__(self, capacity=10000):
self.capacity = capacity
self.data = []
def add(self, transition):
if len(self.data) >= self.capacity:
self.data.pop(0)
self.data.append(transition)
def sample(self, batch_size):
indices = np.random.choice(len(self.data), size=batch_size, replace=False)
return [self.data[i] for i in indices]
def __len__(self):
return len(self.data)Each transition usually has the form:
(s, a, r, sp, done)Target networks
A target network is a delayed copy of the current network. In small NumPy examples, this may be represented by copying parameters:
target_weights = current_weights.copy()Update every fixed number of steps:
if step % target_update_frequency == 0:
target_weights = current_weights.copy()The idea is to stabilize bootstrapping targets.
Dyna-Q model dictionary
Dyna-Q stores a learned model:
model = {}
seen_pairs = []After a real transition:
if (s, a) not in model:
seen_pairs.append((s, a))
model[(s, a)] = (r, sp, done)Planning update:
sim_s, sim_a = random.choice(seen_pairs)
sim_r, sim_sp, sim_done = model[(sim_s, sim_a)]
q_learning_update(Q, sim_s, sim_a, sim_r, sim_sp, sim_done, alpha, gamma)Offline datasets
Offline RL uses a fixed dataset:
rows = []
rows.append({
"episode": ep,
"t": t,
"state": s,
"action": a,
"reward": r,
"next_state": sp,
"done": done
})
data = pd.DataFrame(rows)A transition array:
transitions = data[["state", "action", "reward", "next_state", "done"]].to_numpy()State-action counts:
def state_action_counts(data, n_states, n_actions):
counts = np.zeros((n_states, n_actions), dtype=int)
for _, row in data.iterrows():
s = int(row["state"])
a = int(row["action"])
counts[s, a] += 1
return countsCoverage matters in offline RL.
Behavior cloning
Behavior cloning learns from expert state-action pairs:
expert_data = pd.DataFrame([
{"state": 0, "action": 1},
{"state": 1, "action": 1},
{"state": 2, "action": 0},
])Tabular behavior cloning:
def train_tabular_behavior_cloning(data, n_states, n_actions):
counts = np.zeros((n_states, n_actions), dtype=int)
for _, row in data.iterrows():
counts[int(row["state"]), int(row["action"])] += 1
policy = np.argmax(counts, axis=1)
return policy, countsFor unseen states, use a fallback policy:
if np.sum(counts[s]) == 0:
policy[s] = fallback_policy[s]Preference data for RLHF
Preference data often has the form:
rows.append({
"prompt": x,
"winner": a_w,
"loser": a_l
})Bradley–Terry preference probability:
\[ P(y_w \succ y_l\mid x) = \sigma(r_\theta(x,y_w)-r_\theta(x,y_l)). \]
Sigmoid function:
def sigmoid(z):
return 1.0 / (1.0 + np.exp(-z))Preference loss:
loss = -np.mean(np.log(sigmoid(reward_winner - reward_loser) + 1e-12))KL divergence
For two discrete policies pi and pi_ref:
def mean_kl(pi, pi_ref):
return np.mean(np.sum(pi * (np.log(pi + 1e-12) - np.log(pi_ref + 1e-12)), axis=1))KL regularization appears in RLHF and trust-region policy methods.
Entropy
Policy entropy measures randomness:
def policy_entropy(pi):
return np.mean(-np.sum(pi * np.log(pi + 1e-12), axis=1))Entropy is high when the policy is random and low when the policy is nearly deterministic.
Evaluation loops
Training and evaluation should be separated.
During training, the agent may explore:
a = epsilon_greedy(Q, s, epsilon)During evaluation, use a greedy policy:
a = int(np.argmax(Q[s]))A standard evaluation function:
def evaluate_policy(env, policy, episodes=500, gamma=0.95, seed=0):
set_seed(seed)
returns = []
lengths = []
successes = 0
for ep in range(episodes):
s = env.reset()
done = False
G = 0.0
discount = 1.0
steps = 0
while not done:
a = int(policy[s])
sp, r, done, info = env.step(a)
G += discount * r
discount *= gamma
s = sp
steps += 1
returns.append(G)
lengths.append(steps)
return {
"mean discounted return": float(np.mean(returns)),
"std discounted return": float(np.std(returns)),
"mean episode length": float(np.mean(lengths)),
}Modify this function for success rate, trap rate, or other task-specific metrics.
Training-loop pattern
A standard RL training loop looks like this:
returns = []
lengths = []
for episode in range(n_episodes):
s = env.reset()
done = False
G = 0.0
discount = 1.0
steps = 0
while not done:
a = epsilon_greedy(Q, s, epsilon)
sp, r, done, info = env.step(a)
q_learning_update(Q, s, a, r, sp, done, alpha, gamma)
G += discount * r
discount *= gamma
steps += 1
s = sp
returns.append(G)
lengths.append(steps)This pattern appears throughout the labs.
Common diagnostics
TD error
td_errors.append(abs(td_error))Large TD errors may indicate rapid learning, instability, or poor value estimates.
State visitation
state_counts = np.zeros(env.n_states, dtype=int)
state_counts[s] += 1Useful for exploration and offline coverage.
Action counts
action_counts = np.zeros(env.n_actions, dtype=int)
action_counts[a] += 1Useful for bandits and policy diagnostics.
Success rate
successes.append(final_state == env.goal)Plot:
plt.plot(rolling_mean(successes, window=50))Policy disagreement
Compare a learned policy with a benchmark policy:
disagreement = np.mean(policy != reference_policy)For grid-worlds, exclude walls and terminal states when appropriate.
Numerical stability tips
Avoid log of zero
Use a small constant:
np.log(probs + 1e-12)Stable softmax
Do not write:
np.exp(logits) / np.sum(np.exp(logits))Use:
z = logits - np.max(logits)
probs = np.exp(z) / np.sum(np.exp(z))Avoid singular linear systems
Use ridge regularization:
A_reg = A + 1e-6 * np.eye(A.shape[0])
x = np.linalg.solve(A_reg, b)Clip probabilities if needed
p = np.clip(p, 1e-8, 1.0)Common errors and fixes
Error: index out of bounds
Cause: using a state or action outside the valid range.
Check:
print(s, env.n_states)
print(a, env.n_actions)Valid ranges:
0 <= s < env.n_states
0 <= a < env.n_actions
Error: probabilities do not sum to 1
Cause: numerical or construction error in a probability vector.
Fix:
probs = probs / np.sum(probs)Check:
print(np.sum(probs))Error: NaN values
Check for:
- too large learning rate,
- division by zero,
- log of zero,
- unstable exponentials.
Debug:
np.isnan(Q).any()
np.max(np.abs(Q))Error: policy never reaches the goal
Possible causes:
- insufficient exploration,
- too few episodes,
- reward too sparse,
- learning rate too small or too large,
- bug in environment dynamics,
- terminal states not handled correctly.
Error: notebook results differ from another run
RL is stochastic. Set seeds and compare averages over several seeds.
Suggested code organization
For projects, organize code into sections:
- imports and random seed,
- environment class,
- helper functions,
- algorithm implementation,
- training,
- evaluation,
- visualization,
- comparison table,
- discussion.
Example:
# 1. Imports
# 2. Environment
# 3. Helpers
# 4. Algorithms
# 5. Experiments
# 6. Evaluation
# 7. PlotsStyle guidelines
Use clear names:
state
action
next_state
reward
doneor compact RL names:
s
a
sp
r
doneAvoid mixing conventions in the same notebook.
Use functions for repeated logic:
def train_q_learning(...):
...
def evaluate_policy(...):
...Store histories in dictionaries:
history = {
"returns": np.array(returns),
"lengths": np.array(lengths),
"successes": np.array(successes)
}Minimal Q-learning example
This is a compact template for many tabular control labs.
Q = np.zeros((env.n_states, env.n_actions))
returns = []
for episode in range(500):
epsilon = 0.05 + 0.95 * np.exp(-episode / 200)
s = env.reset()
done = False
G = 0.0
discount = 1.0
while not done:
a = epsilon_greedy(Q, s, epsilon)
sp, r, done, info = env.step(a)
target = r
if not done:
target += gamma * np.max(Q[sp])
Q[s, a] += alpha * (target - Q[s, a])
G += discount * r
discount *= gamma
s = sp
returns.append(G)
policy = np.argmax(Q, axis=1)Minimal REINFORCE example
theta = np.zeros((env.n_states, env.n_actions))
for episode in range(500):
states = []
actions = []
rewards = []
s = env.reset()
done = False
while not done:
probs = softmax(theta[s])
a = np.random.choice(env.n_actions, p=probs)
sp, r, done, info = env.step(a)
states.append(s)
actions.append(a)
rewards.append(r)
s = sp
returns = discounted_returns(rewards, gamma)
for s, a, G in zip(states, actions, returns):
probs = softmax(theta[s])
grad = -probs
grad[a] += 1.0
theta[s] += alpha * G * gradMinimal Dyna-Q example
Q = np.zeros((env.n_states, env.n_actions))
model = {}
seen_pairs = []
for episode in range(500):
s = env.reset()
done = False
while not done:
a = epsilon_greedy(Q, s, epsilon)
sp, r, done, info = env.step(a)
q_learning_update(Q, s, a, r, sp, done, alpha, gamma)
if (s, a) not in model:
seen_pairs.append((s, a))
model[(s, a)] = (r, sp, done)
for _ in range(planning_steps):
sim_s, sim_a = random.choice(seen_pairs)
sim_r, sim_sp, sim_done = model[(sim_s, sim_a)]
q_learning_update(Q, sim_s, sim_a, sim_r, sim_sp, sim_done, alpha, gamma)
s = spMinimal offline fitted Q iteration example
Q = np.zeros((env.n_states, env.n_actions))
transitions = data[["state", "action", "reward", "next_state", "done"]].to_numpy()
for sweep in range(200):
np.random.shuffle(transitions)
for s, a, r, sp, done in transitions:
s = int(s)
a = int(a)
sp = int(sp)
done = bool(done)
target = float(r)
if not done:
target += gamma * np.max(Q[sp])
Q[s, a] += alpha * (target - Q[s, a])Final project coding checklist
Before submitting a project notebook:
Final advice
When debugging reinforcement-learning code, always check the loop:
state -> action -> reward -> next state -> update -> next state becomes current state
Most bugs come from one of the following:
- the state is not updated,
- terminal states are mishandled,
- the wrong next value is used,
- exploration is accidentally turned off,
- arrays have the wrong shape,
- rewards do not match the intended environment,
- evaluation still uses exploratory actions.
When in doubt, print one short trajectory and inspect it carefully.