Using Q-Tables for Decision Making and Encapsulation in Q-Learning

Introduction

Welcome to the third and final lesson of our "Q-Learning Unleashed: Building Intelligent Agents" course! In our previous lessons, we've explored the fundamentals of Q-learning, including how agents learn values for state-action pairs and update their knowledge through interactions with an environment.

So far, we've primarily focused on how Q-learning works and the mathematical foundation behind it. Today, we'll focus on mastering two more crucial aspects:

  1. How to encapsulate our Q-learning algorithm in a well-structured class;
  2. How to use a trained Q-table to make intelligent decisions.

By the end of this lesson, we'll have a complete, reusable Q-learning agent that can navigate environments based on learned knowledge. Let's dive in!

Creating an Object-Oriented Q-Learning Agent

One of the best practices in Reinforcement Learning (and in Machine Learning in general) is to encapsulate our algorithms into classes. This approach provides several benefits:

  • Organization: Keeps related data and functionality together.
  • Reusability: Makes it easy to use the agent in different environments.
  • Extensibility: Allows for straightforward modifications or enhancements.

Let's create a QLearningAgent class that will house our Q-learning algorithm. This class will feature a __init__ constructor method, a learn method that can be used to update the Q-table based on experience, and an act method that is used for decision-making. Starting with the constructor:

Python
from collections import defaultdict
import numpy as np

class QLearningAgent:
    def __init__(self, actions, alpha=0.1, gamma=0.99):
        # List of possible actions
        self.actions = actions
        # Learning rate: controls how quickly the agent updates its Q-values
        self.alpha = alpha
        # Discount factor: determines how much future rewards are valued
        self.gamma = gamma
        # Q-table: maps state-action pairs to estimated values
        self.Q = defaultdict(lambda: np.zeros(len(actions)))

Using a defaultdict is particularly convenient here because we don't need to explicitly check if a state exists in our Q-table before accessing it. If we try to access a state that doesn't exist yet, it will automatically be created with all Q-values initialized to zero.

This approach simplifies our code and allows us to focus on the core learning algorithm rather than data structure management.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal