Welcome to the first lesson of "Q-Learning Unleashed: Building Intelligent Agents"! This is the second course in our "Playing Games with Reinforcement Learning" path, in which we build upon the environment concepts we explored in the previous course. Specifically, in this course we'll dive into one of the most fundamental algorithms in Reinforcement Learning: Q-Learning.
Q-Learning is a model-free Reinforcement Learning algorithm that allows an agent to learn optimal actions through experience. Unlike supervised learning, where we train on labeled examples, in Q-Learning, our agent learns by interacting with an environment and receiving feedback in the form of rewards. Throughout this lesson, we'll explore the concept of policies in reinforcement learning, delve into the mathematical foundations of the Q-Learning algorithm, and implement a Q-table in Python. We'll also write the core Q-Learning update function and see the algorithm in action through practical examples.
Virtually every RL algorithm employs Q-learning, or some variation of it, making it a fundamental tool in your RL toolbox. By the end of this lesson, you'll have a solid understanding of how Q-Learning works and be able to implement your own Q-Learning agent!
Understanding Policies in Reinforcement Learning
The Q-Learning Algorithm
Implementing Q-Tables
To implement Q-Learning, we first need a data structure to store our Q-values. The most straightforward approach is a Q-table — a table where rows represent states and columns represent actions, with each cell containing the corresponding Q-value. While this approach works well for simple environments with discrete states and actions, more advanced approaches use function approximation techniques, such as neural networks in Deep Reinforcement Learning, to estimate Q-values for environments with large or continuous state spaces. Please note we won't deal with Deep RL techniques in this course path.
In Python, we can implement a Q-table using different data structures. For grid-world environments with discrete states and actions, a dictionary or a defaultdict is often the most convenient approach:
This code creates a defaultdict where each key is a state, and each value is a numpy array containing Q-values for each possible action. The beauty of using defaultdict is that it automatically creates entries for states we haven't seen before, initializing their action values to zeros. We can then access the Q-value for a specific state-action pair using Q[state][action].
For example, if our state is represented as a tuple (x, y) for position in a grid, we could access the Q-value for moving right in position (3, 2) with Q[(3, 2)][3], assuming action 3 corresponds to moving right.
The Q-Learning Update Function
Now that we have our Q-table, we need to implement the update rule that will allow our agent to learn from experience. Here's the function that performs the Q-Learning update:
def q_learning_update(Q, state, action, reward, next_state, alpha, gamma, done): """ Q(s,a) <- Q(s,a) + alpha * [ reward + gamma * max_a' Q(s', a') - Q(s,a) ] Parameters: - Q: Q-table (defaultdict mapping states to action value arrays) - state: current state - action: action taken - reward: reward received - next_state: state transitioned to - alpha: learning rate - gamma: discount factor - done: boolean indicating if next_state is terminal """ if done: # For terminal states, there's no future reward to consider best_next_q = 0.0 else: # For non-terminal states, include the discounted future reward best_next_q = np.max(Q[next_state]) Q[state][action] += alpha * (reward + gamma * best_next_q - Q[state][action])
Here's how this function implements the Bellman equation:
The function takes a parameter done which indicates if we've reached a terminal (goal) state or if the maximum number of steps has been reached. This allows our agent to learn differently when it reaches terminal states (like winning or losing a game) or when it hits the maximum steps compared to intermediate states.
For terminal states or when the maximum steps are reached, we set best_next_q = 0.0 because there are no future rewards to consider.
For non-terminal states, we calculate best_next_q = np.max(Q[next_state]) to find the maximum possible Q-value from the next state.
Finally, we update the Q-table using the Bellman equation: Q[state][action] += alpha * (reward + gamma * best_next_q - Q[state][action]).
The learning process is iterative — with each experience, the agent adjusts its Q-values to better reflect the expected rewards.
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal
Before diving into Q-Learning, we need to understand the core concept of policy. In RL, a policy defines the actual agent's behavior by mapping states to actions — essentially, it's the strategy our agent follows, dictating which action the agent should take in each state.
Formally, a policy π(a∣s) represents the probability of taking action a when in state s. There are two main types of policies:
Deterministic policies: The agent always selects the same action in a given state;
Stochastic policies: The agent selects actions with certain probabilities.
The ultimate goal in Reinforcement Learning is to find an optimal policyπ∗ that maximizes the expected cumulative reward, which is the sum of rewards an agent expects to receive over time. This is where Q-Learning comes in — it helps us find this optimal policy without even explicitly modeling the policy itself! Instead, Q-Learning focuses on learning a value function called the Q-function, which estimates how good a particular action is in a given state. Once we have this function, our optimal policy simply becomes "choose the action with the highest Q-value in each state".
Q-Learning is all about estimating a special function called the Q-function, denoted as Q(s,a). This function helps us understand how good it is to take a particular action a when in a specific state s, and then continue following the best possible strategy thereafter.
Think of the Q-function as a guide that tells our agent the potential "value" or "benefit" of making certain decisions in different situations. It's like having a map that shows the expected rewards for each possible path (action) you can take. The goal of Q-Learning is to improve this map over time, so our agent can make better decisions.
The core idea of Q-Learning is to iteratively update our estimates of these Q-values using the Bellman equation:
Q(s,a)←Q(s,a)+α[r+γmaxa′Q(s′,a′)−Q(s,a)]
Let's break down this equation:
Q(s,a) is our current estimate of the Q-value for taking action a in state s.
α (alpha) is the learning rate, which controls how much we adjust our estimates based on new information.
r is the immediate reward received after taking action a.
γ (gamma) is the discount factor, which balances the importance of immediate rewards versus future rewards.
s′ is the next state we transition to after taking action a.
a′ represents all possible actions the agent can take from the next state s′.
maxa′Q(s′,a′) is the maximum Q-value possible from the next state, representing the best future reward we can expect if we choose the best action a′ from s′.
The term [r+γmaxa′Q(s′,a′)−Q(s,a)] is known as the temporal difference error or TD error. It measures the difference between what we expected to happen and what actually happened, allowing us to adjust our Q-values accordingly. We'll discuss the learning rate α and discount factor γ in more detail later in the lesson.
Imagine Q-Learning as learning to navigate a new city: at first, you don't know which routes are the quickest or most scenic. But as you explore and gather experiences, you update your mental map to reflect what you've learned about each street and turn. Over time, this map becomes more accurate, helping you choose the best routes to reach your destinations efficiently.
def q_learning_update(Q, state, action, reward, next_state, alpha, gamma, done): """ Q(s,a) <- Q(s,a) + alpha * [ reward + gamma * max_a' Q(s', a') - Q(s,a) ] Parameters: - Q: Q-table (defaultdict mapping states to action value arrays) - state: current state - action: action taken - reward: reward received - next_state: state transitioned to - alpha: learning rate - gamma: discount factor - done: boolean indicating if next_state is terminal """ if done: # For terminal states, there's no future reward to consider best_next_q = 0.0 else: # For non-terminal states, include the discounted future reward best_next_q = np.max(Q[next_state]) Q[state][action] += alpha * (reward + gamma * best_next_q - Q[state][action])
Two critical parameters in Q-Learning deserve special attention as they dramatically affect how our agent learns:
Learning Rate (α):
Controls how quickly the agent updates its Q-values.
A higher α (e.g., 0.9) makes the agent adapt quickly but might lead to instability.
A lower α (e.g., 0.1) makes learning more stable but slower.
Typical values range from 0.01 to 0.5.
Think of α as adjusting how stubborn your agent is. With `α=1, the agent completely overwrites old knowledge with new experiences. With α=0.1, the agent mostly sticks to what it already knows, making only small adjustments based on new information.
Discount Factor (γ):
Determines the importance of future rewards versus immediate rewards.
A value close to 0 makes the agent "myopic" or short-sighted, focusing only on immediate rewards (try plugging in γ=0 in the Bellman equation, and see the result!).
A value close to 1 makes the agent value future rewards almost as much as immediate ones.
Typical values range from 0.9 to 0.99.
Imagine γ as controlling how far ahead your agent plans. A chess player with γ=0 would only care about capturing the opponent's piece right now, while a player with γ=0.99 would sacrifice immediate captures to set up checkmate several moves ahead.
Let's see how our Q-Learning update works with a concrete example. Imagine a robot navigating a grid-world, trying to reach a charging station:
Python
# First experience: moving right to reach the goalstate = (3, 2) # Robot is one cell left of the goalaction = 3 # Action 3 is "move right"reward = 1.0 # Positive reward for reaching goalnext_state = (3, 3) # New state is the goal positiondone = True # This is a terminal state (reaching the goal)alpha = 0.1 # Learning rategamma = 0.99 # Discount factorprint("Q-table before update:")print(f"Q[{state}] = {Q[state]}")# Output: Q[(3, 2)] = [0. 0. 0. 0.]q_learning_update(Q, state, action, reward, next_state, alpha, gamma, done)print("\nQ-table after update (positive reward):")print(f"Q[{state}] = {Q[state]}")# Output: Q[(3, 2)] = [0. 0. 0. 0.1]
Initially, all Q-values are zero because the robot has no experience. After moving right and receiving a (positive) reward, the Q-value for "move right" in state (3, 2) increases to 0.1. This tells the robot that moving right from this position is promising.
Let's see what happens with a negative experience:
Python
# Second experience: moving left leads away from goalstate = (3, 2) # Robot is at the same positionaction = 2 # Action 2 is "move left"reward = -1.0 # Negative reward for moving away from goalnext_state = (2, 2) # New state is further from goaldone = False # This is not a terminal stateq_learning_update(Q, state, action, reward, next_state, alpha, gamma, done)print("\nQ-table after update (negative reward):")print(f"Q[{state}] = {Q[state]}")# Output: Q[(3, 2)] = [ 0. 0. -0.1 0.1]
After this negative experience (a negative reward or "punishment"), the Q-value for "move left" drops to −0.1, while "move right" still has a positive value of 0.1. The robot is learning that moving right from state (3, 2) is better than moving left.
If the robot follows a greedy policy (always choosing the action with the highest Q-value), it would now choose to move right when in state (3, 2) — exactly what we want! This demonstrates how Q-Learning helps agents discover optimal behaviors through trial and error.
In this lesson, we've explored Q-Learning, a fundamental Reinforcement Learning algorithm that enables agents to learn optimal behaviors through experience. We delved into the concept of policies, which guide agent behavior, and examined the mathematical foundation of Q-Learning through the Bellman equation. By implementing a Q-table using Python's defaultdict and creating a Q-Learning update function, we saw how agents can learn from experience and adjust their actions based on rewards. We also discussed the importance of the learning rate (α) and discount factor (γ) in shaping the agent's learning process.
Q-Learning's strength lies in its simplicity and its ability to discover optimal behaviors without requiring a model of the environment. Through practical examples, we also observed how Q-values evolve based on positive and negative experiences, guiding agents toward better decision-making. As you move on to the practice exercises, you'll gain hands-on experience with Q-Learning concepts, reinforcing your understanding by implementing these theoretical ideas in code. Remember, mastering Q-Learning takes practice, so embrace the challenges and build your confidence as you work through the examples.