Implement the three action-value control updates side by side. They differ only in how the next state is valued.
Every one of these has the same skeleton — move Q(s,a) a fraction alpha toward a target — and the entire on-policy/off-policy distinction lives in one term.
SARSA uses the action the behaviour policy actually took next:
Q(St,At)←Q(St,At)+α[Rt+1+γQ(St+1,At+1)−Q(St,At)]
It is on-policy: it learns the value of the policy you are running, exploration mistakes included. That is why SARSA learns the "safe" path near a cliff — it knows an epsilon-greedy agent will occasionally fall off.
Q-learning ignores what happened next and uses the greedy value:
Q(St,At)←Q(St,At)+α[Rt+1+γmaxa′Q(St+1,a′)−Q(St,At)]
It is off-policy: it learns the optimal policy's values while behaving with exploration. The price is maximisation bias — a max over noisy estimates is systematically optimistic.
Expected SARSA replaces the sample or the max with the exact expectation under the target policy:
Q(St,At)←Q(St,At)+α[Rt+1+γ∑a′π(a′∣St+1)Q(St+1,a′)−Q(St,At)]
Removing the sampling of At+1 removes its variance for the cost of one extra sum, which is usually free. With an epsilon-greedy π and epsilon = 0 it is Q-learning; with larger epsilon it drifts toward the on-policy answer.
Implement all three. Each returns the new scalar value of Q[s][a]; do not mutate Q.
def sarsa_update(Q, s, a, r, s2, a2, alpha, gamma):
...
def q_learning_update(Q, s, a, r, s2, alpha, gamma):
...
def expected_sarsa_update(Q, s, a, r, s2, alpha, gamma, epsilon):
...
expected_sarsa_update weights the successor action values by an **epsilon-greedy distribution over **Q[s2]****, splitting the greedy probability 1 - epsilon evenly across all tied maxima.
Q is a nested list of floats. Each function returns a single float, rounded to 4 decimals by the grader.
Q = [[0.0, 0.0], [2.0, 6.0]]
print(round(sarsa_update(Q, 0, 0, 1.0, 1, 0, 0.5, 0.9), 4))
print(round(q_learning_update(Q, 0, 0, 1.0, 1, 0.5, 0.9), 4))
print(round(expected_sarsa_update(Q, 0, 0, 1.0, 1, 0.5, 0.9, 0.2), 4))
Output:
1.4
3.2
3.02
All three start from Q[0][0] = 0.0 and move half way (alpha = 0.5) toward their target. SARSA bootstraps off the action actually taken, Q[1][0] = 2.0, for a target of 1.0 + 0.9*2.0 = 2.8. Q-learning bootstraps off max(Q[1]) = 6.0, for a target of 6.4. Expected SARSA bootstraps off the epsilon-greedy expectation 0.12.0 + 0.96.0 = 5.6, for a target of 1.0 + 0.9*5.6 = 6.04.
Q = [[0.0, 0.0], [2.0, 6.0]]; sarsa_update(Q, 0, 0, 1.0, 1, 0, 0.5, 0.9), q_learning_update(Q, 0, 0, 1.0, 1, 0.5, 0.9), expected_sarsa_update(Q, 0, 0, 1.0, 1, 0.5, 0.9, 0.2)
1.4 3.2 3.02
All three move Q(0,0)=0 half way toward their target because alpha=0.5. SARSA uses the action actually taken next, a2=0, so its target is 1.0 + 0.92.0 = 2.8 and the result is 1.4. Q-learning uses max(Q[1]) = 6.0 for a target of 6.4, giving 3.2. Expected SARSA weights the successor values by the epsilon-greedy distribution: with epsilon=0.2 over 2 actions the greedy action gets 0.8 + 0.1 = 0.9 and the other gets 0.1, so the expectation is 0.12.0 + 0.96.0 = 5.6, the target is 1.0 + 0.95.6 = 6.04, and the result is 3.02.
1 <= n_states <= 500, 1 <= n_actions <= 500.0 <= alpha <= 1.0, 0.0 <= gamma <= 1.0, 0.0 <= epsilon <= 1.0Q must not be modified — return the new scalar only.1 - epsilon evenly.