训练智能体

当我们谈论训练强化学习 (RL) 智能体时,我们是指通过经验教会它做出良好决策。与监督学习中我们展示正确答案的例子不同,RL 智能体通过尝试不同的动作并观察结果来学习。这就像学习骑自行车——你尝试不同的动作,摔倒几次,然后逐渐学会什么有效。

目标是制定一个**策略**——一个告诉智能体在每种情况下采取何种动作以最大化长期奖励的策略。

直观理解 Q 学习

在本教程中,我们将使用 Q 学习来解决二十一点环境。但首先,让我们概念性地理解 Q 学习是如何工作的。

Q 学习构建一个巨大的“作弊表”,称为 Q 表,它告诉智能体在每种情况下每个动作有多好。

  • = 智能体可能遇到的不同情况(状态)

  • = 智能体可以采取的不同动作

  • = 在那种情况下该动作有多好(预期的未来奖励)

对于二十一点

  • 状态:你的手牌点数,庄家翻开的牌,你是否拥有可用 A

  • 动作:要牌 (再拿一张牌) 或停牌 (保留当前手牌)

  • Q 值:在每个状态下每个动作的预期奖励

学习过程

  1. 尝试一个动作,看看会发生什么(奖励 + 新状态)

  2. 更新你的作弊表:“那个动作比我想象的要好/差”

  3. 逐渐改进通过尝试动作和更新估计

  4. 平衡探索与利用:尝试新事物与利用已知有效的方法

为什么它有效:随着时间的推移,好的动作会获得更高的 Q 值,坏的动作会获得更低的 Q 值。智能体学会选择具有最高预期奖励的动作。


本页面简要概述了如何为 Gymnasium 环境训练智能体。我们将使用表格 Q 学习来解决 Blackjack-v1。有关其他环境和算法的完整教程,请参阅训练教程。请在此页面之前阅读基本用法

关于环境:二十一点

二十一点是最受欢迎的赌场纸牌游戏之一,非常适合学习强化学习,因为它具有

  • 清晰的规则:在不超过 21 点的情况下,点数比庄家更接近 21

  • 简单的观察:你的手牌点数,庄家翻开的牌,可用 A

  • 离散动作:要牌 (拿牌) 或停牌 (保留当前手牌)

  • 即时反馈:每手牌后输赢或平局

此版本使用无限牌组(抽到的牌会放回),因此算牌无效——智能体必须通过试错学习最佳基本策略。

环境细节:

  • 观察:(玩家总点数, 庄家牌, 可用 A)

    • player_sum: 当前手牌点数 (4-21)

    • dealer_card: 庄家面朝上的牌 (1-10)

    • usable_ace: 玩家是否拥有可用 A (真/假)

  • 动作:0 = 停牌,1 = 要牌

  • 奖励:赢 +1,输 -1,平局 0

  • 回合结束:当玩家停牌或爆牌 (超过 21 点)

执行动作

env.reset() 接收到第一次观察后,我们使用 env.step(action) 与环境交互。此函数接收一个动作并返回五个重要值

observation, reward, terminated, truncated, info = env.step(action)
  • observation: 智能体在采取动作后看到的内容 (新的游戏状态)

  • reward: 该动作的即时反馈 (二十一点中为 +1, -1, 或 0)

  • terminated: 回合是否自然结束 (手牌已结束)

  • truncated: 回合是否被截断 (时间限制 - 二十一点中未使用)

  • info: 额外的调试信息 (通常可以忽略)

关键的见解是,reward 告诉我们即时动作有多好,但智能体需要学习长期后果。Q 学习通过估计总未来奖励,而不仅仅是即时奖励来处理这个问题。

构建 Q 学习智能体

让我们一步步构建我们的智能体。我们需要以下功能:

  1. 选择动作 (兼顾探索与利用)

  2. 从经验中学习 (更新 Q 值)

  3. 管理探索 (随时间减少随机性)

探索与利用

这是强化学习中的一个根本挑战

  • 探索:尝试新的动作以了解环境

  • 利用:利用现有知识获得最佳奖励

我们使用epsilon-贪婪策略

  • 以概率 epsilon:选择一个随机动作(探索)

  • 以概率 1-epsilon:选择已知最佳动作(利用)

从高 epsilon 开始(大量探索)并逐渐减少它(随着学习而更多利用)在实践中效果很好。

from collections import defaultdict
import gymnasium as gym
import numpy as np


class BlackjackAgent:
    def __init__(
        self,
        env: gym.Env,
        learning_rate: float,
        initial_epsilon: float,
        epsilon_decay: float,
        final_epsilon: float,
        discount_factor: float = 0.95,
    ):
        """Initialize a Q-Learning agent.

        Args:
            env: The training environment
            learning_rate: How quickly to update Q-values (0-1)
            initial_epsilon: Starting exploration rate (usually 1.0)
            epsilon_decay: How much to reduce epsilon each episode
            final_epsilon: Minimum exploration rate (usually 0.1)
            discount_factor: How much to value future rewards (0-1)
        """
        self.env = env

        # Q-table: maps (state, action) to expected reward
        # defaultdict automatically creates entries with zeros for new states
        self.q_values = defaultdict(lambda: np.zeros(env.action_space.n))

        self.lr = learning_rate
        self.discount_factor = discount_factor  # How much we care about future rewards

        # Exploration parameters
        self.epsilon = initial_epsilon
        self.epsilon_decay = epsilon_decay
        self.final_epsilon = final_epsilon

        # Track learning progress
        self.training_error = []

    def get_action(self, obs: tuple[int, int, bool]) -> int:
        """Choose an action using epsilon-greedy strategy.

        Returns:
            action: 0 (stand) or 1 (hit)
        """
        # With probability epsilon: explore (random action)
        if np.random.random() < self.epsilon:
            return self.env.action_space.sample()

        # With probability (1-epsilon): exploit (best known action)
        else:
            return int(np.argmax(self.q_values[obs]))

    def update(
        self,
        obs: tuple[int, int, bool],
        action: int,
        reward: float,
        terminated: bool,
        next_obs: tuple[int, int, bool],
    ):
        """Update Q-value based on experience.

        This is the heart of Q-learning: learn from (state, action, reward, next_state)
        """
        # What's the best we could do from the next state?
        # (Zero if episode terminated - no future rewards possible)
        future_q_value = (not terminated) * np.max(self.q_values[next_obs])

        # What should the Q-value be? (Bellman equation)
        target = reward + self.discount_factor * future_q_value

        # How wrong was our current estimate?
        temporal_difference = target - self.q_values[obs][action]

        # Update our estimate in the direction of the error
        # Learning rate controls how big steps we take
        self.q_values[obs][action] = (
            self.q_values[obs][action] + self.lr * temporal_difference
        )

        # Track learning progress (useful for debugging)
        self.training_error.append(temporal_difference)

    def decay_epsilon(self):
        """Reduce exploration rate after each episode."""
        self.epsilon = max(self.final_epsilon, self.epsilon - self.epsilon_decay)

理解 Q 学习更新

核心学习发生在 update 方法中。让我们分解一下数学

# Current estimate: Q(state, action)
current_q = self.q_values[obs][action]

# What we actually experienced: reward + discounted future value
target = reward + self.discount_factor * max(self.q_values[next_obs])

# How wrong were we?
error = target - current_q

# Update estimate: move toward the target
new_q = current_q + learning_rate * error

这就是著名的**贝尔曼方程**的实际应用——它表明状态-动作对的值应该等于即时奖励加上最佳下一个动作的折扣值。

训练智能体

现在让我们训练我们的智能体。过程如下:

  1. 重置环境以开始新回合

  2. 玩一整手牌(一回合),选择动作并从每一步中学习

  3. 更新探索率(减少 epsilon)

  4. 重复多个回合,直到智能体学会好的策略

# Training hyperparameters
learning_rate = 0.01        # How fast to learn (higher = faster but less stable)
n_episodes = 100_000        # Number of hands to practice
start_epsilon = 1.0         # Start with 100% random actions
epsilon_decay = start_epsilon / (n_episodes / 2)  # Reduce exploration over time
final_epsilon = 0.1         # Always keep some exploration

# Create environment and agent
env = gym.make("Blackjack-v1", sab=False)
env = gym.wrappers.RecordEpisodeStatistics(env, buffer_length=n_episodes)

agent = BlackjackAgent(
    env=env,
    learning_rate=learning_rate,
    initial_epsilon=start_epsilon,
    epsilon_decay=epsilon_decay,
    final_epsilon=final_epsilon,
)

训练循环

from tqdm import tqdm  # Progress bar

for episode in tqdm(range(n_episodes)):
    # Start a new hand
    obs, info = env.reset()
    done = False

    # Play one complete hand
    while not done:
        # Agent chooses action (initially random, gradually more intelligent)
        action = agent.get_action(obs)

        # Take action and observe result
        next_obs, reward, terminated, truncated, info = env.step(action)

        # Learn from this experience
        agent.update(obs, action, reward, terminated, next_obs)

        # Move to next state
        done = terminated or truncated
        obs = next_obs

    # Reduce exploration rate (agent becomes less random over time)
    agent.decay_epsilon()

训练期间的预期

早期回合 (0-10,000):

  • 智能体主要随机行动 (高 epsilon)

  • 约 43% 的手牌获胜(由于策略不佳,略逊于随机)

  • 由于 Q 值非常不准确,学习误差较大

中期回合 (10,000-50,000):

  • 智能体开始找到好的策略

  • 胜率提高到 45-48%

  • 随着估计的改善,学习误差减小

后期回合 (50,000+):

  • 智能体收敛到接近最优的策略

  • 胜率稳定在 49% 左右(该游戏的理论最大值)

  • Q 值稳定后,学习误差很小

分析训练结果

让我们可视化训练进度

from matplotlib import pyplot as plt

def get_moving_avgs(arr, window, convolution_mode):
    """Compute moving average to smooth noisy data."""
    return np.convolve(
        np.array(arr).flatten(),
        np.ones(window),
        mode=convolution_mode
    ) / window

# Smooth over a 500-episode window
rolling_length = 500
fig, axs = plt.subplots(ncols=3, figsize=(12, 5))

# Episode rewards (win/loss performance)
axs[0].set_title("Episode rewards")
reward_moving_average = get_moving_avgs(
    env.return_queue,
    rolling_length,
    "valid"
)
axs[0].plot(range(len(reward_moving_average)), reward_moving_average)
axs[0].set_ylabel("Average Reward")
axs[0].set_xlabel("Episode")

# Episode lengths (how many actions per hand)
axs[1].set_title("Episode lengths")
length_moving_average = get_moving_avgs(
    env.length_queue,
    rolling_length,
    "valid"
)
axs[1].plot(range(len(length_moving_average)), length_moving_average)
axs[1].set_ylabel("Average Episode Length")
axs[1].set_xlabel("Episode")

# Training error (how much we're still learning)
axs[2].set_title("Training Error")
training_error_moving_average = get_moving_avgs(
    agent.training_error,
    rolling_length,
    "same"
)
axs[2].plot(range(len(training_error_moving_average)), training_error_moving_average)
axs[2].set_ylabel("Temporal Difference Error")
axs[2].set_xlabel("Step")

plt.tight_layout()
plt.show()

解释结果

奖励图:应显示从约 -0.05(略为负值)到约 -0.01(接近最优)的逐渐改善。二十一点是一个困难的游戏——即使完美发挥,由于庄家优势也会略微亏损。

回合长度:应稳定在每回合 2-3 个动作。回合太短表示智能体过早停牌;回合太长表示要牌过于频繁。

训练误差:应随时间减少,表明智能体的预测越来越准确。训练早期出现的大幅峰值是正常的,因为智能体会遇到新的情况。

常见训练问题及解决方案

🚨 智能体从未改进

症状:奖励保持不变,训练误差大 原因:学习率过高/过低,奖励设计不佳,更新逻辑中存在错误 解决方案

  • 尝试 0.001 到 0.1 之间的学习率

  • 检查奖励是否具有意义(二十一点中为 -1, 0, +1)

  • 验证 Q 表是否确实在更新

🚨 训练不稳定

症状:奖励波动剧烈,从不收敛 原因:学习率过高,探索不足 解决方案

  • 降低学习率(尝试 0.01 而不是 0.1)

  • 确保最小探索(final_epsilon ≥ 0.05)

  • 训练更多回合

🚨 智能体陷入不良策略

症状:改进过早停止,最终表现不理想 原因:探索过少,学习率过低 解决方案

  • 增加探索时间(更慢的 epsilon 衰减)

  • 初期尝试更高的学习率

  • 使用不同的探索策略(乐观初始化)

🚨 学习太慢

症状:智能体有改进,但非常缓慢 原因:学习率过低,探索过多 解决方案

  • 提高学习率(但要注意不稳定性)

  • 更快的 epsilon 衰减(更少随机探索)

  • 更集中地训练困难状态

测试训练好的智能体

训练完成后,测试智能体的表现

# Test the trained agent
def test_agent(agent, env, num_episodes=1000):
    """Test agent performance without learning or exploration."""
    total_rewards = []

    # Temporarily disable exploration for testing
    old_epsilon = agent.epsilon
    agent.epsilon = 0.0  # Pure exploitation

    for _ in range(num_episodes):
        obs, info = env.reset()
        episode_reward = 0
        done = False

        while not done:
            action = agent.get_action(obs)
            obs, reward, terminated, truncated, info = env.step(action)
            episode_reward += reward
            done = terminated or truncated

        total_rewards.append(episode_reward)

    # Restore original epsilon
    agent.epsilon = old_epsilon

    win_rate = np.mean(np.array(total_rewards) > 0)
    average_reward = np.mean(total_rewards)

    print(f"Test Results over {num_episodes} episodes:")
    print(f"Win Rate: {win_rate:.1%}")
    print(f"Average Reward: {average_reward:.3f}")
    print(f"Standard Deviation: {np.std(total_rewards):.3f}")

# Test your agent
test_agent(agent, env)

二十一点的良好表现

  • 胜率:42-45%(庄家优势使得 >50% 不可能)

  • 平均奖励:-0.02 到 +0.01

  • 一致性:低标准差表示策略可靠

下一步

恭喜!你已成功训练了你的第一个强化学习智能体。接下来可以探索以下内容:

  1. 尝试其他环境:CartPole (小车杆), MountainCar (登山车), LunarLander (月球着陆器)

  2. 实验超参数:学习率、探索策略

  3. 实现其他算法:SARSA, Expected SARSA, Monte Carlo 方法

  4. 添加函数逼近:用于更大状态空间的神经网络

  5. 创建自定义环境:设计你自己的强化学习问题

更多信息,请参阅

本教程的关键在于,强化学习智能体通过试错学习,逐渐积累关于在不同情境下哪些动作效果最佳的知识。Q 学习提供了一种系统地学习这些知识的方法,平衡了新可能性的探索和现有知识的利用。

继续实验,请记住,强化学习既是科学也是艺术——找到合适的超参数和环境设计通常需要耐心和直觉!