Navigation

Introduction to AI

Machine Learning

Deep Learning

Generative AI

Tools & Frameworks

General

Reinforcement Learning Basics: Foundations of Agent Learning

Reinforcement learning (RL) is a paradigm of machine learning where an agent learns to make decisions by interacting with an environment. Unlike supervised learning, RL doesn’t require labeled data — the agent learns from the consequences of its actions.

The RL Framework

An RL agent operates within an environment, taking actions and receiving rewards:

class RLEnvironment:
    def reset(self):
        """Return initial state."""
        pass
    
    def step(self, action):
        """Apply action, return (next_state, reward, done)."""
        pass
    
    def render(self):
        """Visualize the environment."""
        pass

class RLAgent:
    def select_action(self, state):
        """Choose an action based on current state."""
        pass
    
    def update(self, state, action, reward, next_state):
        """Learn from the transition."""
        pass

The Markov Decision Process

RL problems are formalized as Markov Decision Processes (MDPs):

class MDP:
    def __init__(self, states, actions, transition_probs, rewards, gamma, horizon=None):
        self.states = states              # State space
        self.actions = actions            # Action space
        self.P = transition_probs         # P(s'|s,a): transition probabilities
        self.R = rewards                  # R(s,a): expected reward
        self.gamma = gamma                # Discount factor (0-1)
        self.H = horizon                  # Maximum steps (None = infinite)

The Reward Signal

# Example: CartPole environment reward
def cartpole_reward(state, action, next_state):
    """Reward is 1 for each step the pole stays upright."""
    angle = next_state[2]
    return 1.0 if abs(angle) < 0.15 else 0.0

# Example: Chess reward
def chess_reward(outcome):
    """
    +1 for win, -1 for loss, 0 otherwise
    (intermediate rewards are sparse in chess)
    """
    return outcome

Value Functions

Value functions estimate how good it is to be in a state or take an action:

class ValueFunction:
    def value(self, state):
        """V(s): Expected return from state s following policy π."""
        pass

class ActionValueFunction:
    def q_value(self, state, action):
        """Q(s,a): Expected return from taking action a in state s."""
        pass

The Bellman Equations

The Bellman equation expresses value functions recursively:

def bellman_optimality(state, value_function, mdp):
    """
    V*(s) = max_a [ R(s,a) + γ * Σ P(s'|s,a) * V*(s') ]
    """
    q_values = []
    for action in mdp.actions:
        expected_reward = mdp.R[state][action]
        future_value = sum(
            mdp.P[state][action][next_state] * value_function.value(next_state)
            for next_state in mdp.states
        )
        q_values.append(expected_future_value := expected_reward + mdp.gamma * future_value)
    
    return max(q_values)

Q-Learning: Learning Action Values

Q-learning learns the optimal action-value function directly:

class QLearning:
    def __init__(self, state_dim, action_dim, learning_rate=0.1, gamma=0.99):
        self.Q = initialize_q_table(state_dim, action_dim)
        self.lr = learning_rate
        self.gamma = gamma
    
    def select_action(self, state, epsilon=0.1):
        """ε-greedy action selection."""
        if random.random() < epsilon:
            return random.choice(range(self.action_dim))
        return argmax(self.Q[state])
    
    def update(self, state, action, reward, next_state):
        """
        Q(s,a) ← Q(s,a) + α [ r + γ * max_a' Q(s',a') - Q(s,a) ]
        """
        current_q = self.Q[state][action]
        max_next_q = max(self.Q[next_state])
        td_target = reward + self.gamma * max_next_q
        td_error = td_target - current_q
        self.Q[state][action] = current_q + self.lr * td_error

Deep Q-Networks (DQN)

For large state spaces, use neural networks to approximate Q-values:

class DQN(nn.Module):
    def __init__(self, state_dim, action_dim, hidden_dim=128):
        super().__init__()
        self.network = nn.Sequential(
            nn.Linear(state_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, action_dim)
        )
    
    def forward(self, x):
        return self.network(x)

class DeepQLearning:
    def __init__(self, state_dim, action_dim):
        self.q_network = DQN(state_dim, action_dim)
        self.target_network = DQN(state_dim, action_dim)
        self.optimizer = torch.optim.Adam(self.q_network.parameters())
        self.replay_buffer = ReplayBuffer(capacity=100000)
    
    def update(self, batch_size=32):
        # Sample from replay buffer
        states, actions, rewards, next_states, dones = self.replay_buffer.sample(batch_size)
        
        # Compute current Q values
        current_q = self.q_network(states).gather(1, actions)
        
        # Compute target Q values (with target network)
        with torch.no_grad():
            next_q = self.target_network(next_states).max(1)[0]
            target_q = rewards + self.gamma * next_q * (1 - dones)
        
        # Compute loss and update
        loss = F.mse_loss(current_q, target_q.unsqueeze(1))
        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()
    
    def update_target_network(self):
        """Copy weights from Q network to target network."""
        self.target_network.load_state_dict(self.q_network.state_dict())

Policy Gradient Methods

Instead of learning values, directly optimize the policy:

class PolicyGradient:
    def __init__(self, policy_network, lr=0.01):
        self.policy = policy_network
        self.optimizer = torch.optim.Adam(policy_network.parameters(), lr=lr)
    
    def select_action(self, state):
        """Sample action from policy distribution."""
        probs = self.policy(state)
        dist = Categorical(probs)
        return dist.sample().item()
    
    def update(self, trajectories):
        """
        Policy Gradient: ∇J(θ) = E[ ∇log π(a|s) * Q(s,a) ]
        
        trajectories: list of (state, action, reward, ...) sequences
        """
        returns = []
        for traj in trajectories:
            # Compute discounted returns
            R = 0
            returns_traj = []
            for r in reversed(traj.rewards):
                R = r + self.gamma * R
                returns_traj.append(R)
            returns.extend(reversed(returns_traj))
        
        returns = torch.tensor(returns)
        returns = (returns - returns.mean()) / (returns.std() + 1e-8)  # Normalize
        
        # Compute policy loss
        loss = 0
        for t, (state, action, R) in enumerate(zip(traj.states, traj.actions, returns)):
            log_prob = torch.log(self.policy(state)[action])
            loss += -log_prob * R  # Negative because we maximize
        
        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()

Proximal Policy Optimization (PPO)

PPO is a simple and effective policy gradient method:

class PPO:
    def __init__(self, policy, clip_epsilon=0.2, lr=3e-4):
        self.policy = policy
        self.clip_epsilon = clip_epsilon
        self.optimizer = torch.optim.Adam(policy.parameters(), lr=lr)
    
    def update(self, states, actions, old_log_probs, advantages, returns, epochs=10):
        """Update policy using PPO clipped objective."""
        for _ in range(epochs):
            # Get new action distribution
            new_probs = self.policy(states)
            dist = Categorical(new_probs)
            new_log_probs = dist.log_prob(actions)
            
            # Compute ratio
            ratio = torch.exp(new_log_probs - old_log_probs)
            
            # Clipped objective
            surr1 = ratio * advantages
            surr2 = torch.clamp(ratio, 1 - self.clip_epsilon, 1 + self.clip_epsilon)
            policy_loss = -torch.min(surr1, surr2).mean()
            
            # Value loss
            values = self.policy.value(states)
            value_loss = F.mse_loss(values, returns)
            
            # Total loss
            loss = policy_loss + 0.5 * value_loss
            
            self.optimizer.zero_grad()
            loss.backward()
            self.optimizer.step()

Exploration vs Exploitation

Balancing exploration and exploitation is key in RL:

class ExplorationStrategies:
    def epsilon_greedy(self, state, Q, epsilon=0.1):
        if random.random() < epsilon:
            return random_action()
        return argmax(Q(state))
    
    def ucb(self, state, Q, counts, c=2):
        """Upper Confidence Bound: prefer less-tried actions."""
        u = c * sqrt(log(sum(counts)) / counts)
        return argmax(Q(state) + u)
    
    def boltzmann(self, state, Q, temperature=1.0):
        """Softmax over Q values."""
        exp_q = exp(Q(state) / temperature)
        probs = exp_q / sum(exp_q)
        return sample(probs)

Training RL Agents

class RLTrainer:
    def __init__(self, env, agent):
        self.env = env
        self.agent = agent
    
    def train(self, num_episodes=1000, verbose_interval=100):
        returns = []
        
        for episode in range(num_episodes):
            state = self.env.reset()
            total_reward = 0
            
            while not done:
                action = self.agent.select_action(state)
                next_state, reward, done = self.env.step(action)
                
                # Store and update
                self.agent.replay_buffer.add(state, action, reward, next_state, done)
                if len(self.agent.replay_buffer) > batch_size:
                    self.agent.update()
                
                state = next_state
                total_reward += reward
            
            returns.append(total_reward)
            
            if episode % verbose_interval == 0:
                avg_return = mean(returns[-verbose_interval:])
                print(f"Episode {episode}: Avg Return = {avg_return:.2f}")
        
        return returns

Reinforcement learning powers some of the most impressive AI achievements — from game playing to robotics. Understanding these fundamentals opens the door to building agents that learn from experience rather than data.