训练智能体#
本页面简要介绍了如何为 Gymnasium 环境训练智能体(agent),特别是我们将使用基于表格的 Q-learning 来解决 Blackjack v1 环境。有关此教程的完整版本以及更多其他环境和算法的训练教程,请参阅此处。在阅读本页之前,请先阅读基本用法。在我们实现任何代码之前,这里是 Blackjack 和 Q-learning 的概述。
Blackjack 是最受欢迎的赌场纸牌游戏之一,也因在某些条件下可被击败而臭名昭著。这款游戏的版本使用无限副牌(我们替换抽牌),因此在我们模拟的游戏中数牌不是一个可行的策略。观察值是一个元组,包括玩家当前的总点数、庄家面朝上的牌的值以及一个布尔值,表示玩家是否持有可用的牌。智能体可以在两个动作之间选择:站立(0)意味着玩家不再拿牌,击打(1)意味着玩家将再拿一张牌。要赢,你的牌的总和应该大于庄家的牌的总和且不超过21。如果玩家选择站立或牌的总和超过21,游戏结束。完整的文档可以在 toy_text/blackjack 找到。
Q-learning 是一种由 Watkins 于 1989 年提出的无模型离策略学习算法,适用于具有离散动作空间的环境,并因其是第一个在一定条件下证明收敛到最优策略的强化学习算法而闻名。
执行动作#
在接收到第一个观察值之后,我们将使用 env.step(action)
函数与环境进行交互。该函数接受动作作为输入并在环境中执行它。因为这个动作会改变环境的状态,所以它会返回四个有用的变量给我们。这些是:
下一个观测值:这是智能体在采取动作后将接收到的观察值。
奖励:这是智能体在采取动作后将接收到的奖励。
终止:这是一个布尔变量,指示环境是否已经因内部条件而终止(即结束)。
截断:这是一个布尔变量,也指示情节是否由于提前截断而结束,即达到了时间限制。
信息:这是一个可能包含有关环境的额外信息的字典。
下一个观测值(next observation
)、奖励(reward
)、终止(terminated
)和截断(truncated
)变量是不言自明的,但信息变量需要一些额外的解释。这个变量包含一个字典,可能有一些关于环境的额外信息,但在 Blackjack-v1 环境中你可以忽略它。例如,在雅达利(Atari)环境中,信息字典有 ale.lives
键,告诉我们智能体还剩下多少条命。如果智能体没有生命了,那么情节就结束了。
注意,在你的训练循环中调用 env.render()
不是好主意,因为渲染会大大减慢训练速度。相反,尝试构建一个额外的循环来评估和展示训练后的智能体。
构建智能体#
让我们来构建用于解决 Blackjack 的 Q-learning 智能体!我们需要一些函数来选择动作和更新智能体的动作值。为了确保智能体能探索环境,可能的解决方案是 epsilon-greedy 策略,在这种策略中,我们以 epsilon
的概率随机选择一个动作,以 1 - 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 Reinforcement Learning agent with an empty dictionary
of state-action values (q_values), a learning rate and an epsilon.
Args:
env: The training environment
learning_rate: The learning rate
initial_epsilon: The initial epsilon value
epsilon_decay: The decay for epsilon
final_epsilon: The final epsilon value
discount_factor: The discount factor for computing the Q-value
"""
self.env = env
self.q_values = defaultdict(lambda: np.zeros(env.action_space.n))
self.lr = learning_rate
self.discount_factor = discount_factor
self.epsilon = initial_epsilon
self.epsilon_decay = epsilon_decay
self.final_epsilon = final_epsilon
self.training_error = []
def get_action(self, obs: tuple[int, int, bool]) -> int:
"""
Returns the best action with probability (1 - epsilon)
otherwise a random action with probability epsilon to ensure exploration.
"""
# with probability epsilon return a random action to explore the environment
if np.random.random() < self.epsilon:
return self.env.action_space.sample()
# with probability (1 - epsilon) act greedily (exploit)
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],
):
"""Updates the Q-value of an action."""
future_q_value = (not terminated) * np.max(self.q_values[next_obs])
temporal_difference = (
reward + self.discount_factor * future_q_value - self.q_values[obs][action]
)
self.q_values[obs][action] = (
self.q_values[obs][action] + self.lr * temporal_difference
)
self.training_error.append(temporal_difference)
def decay_epsilon(self):
self.epsilon = max(self.final_epsilon, self.epsilon - self.epsilon_decay)
训练智能体#
为了训练智能体,我们将让智能体一次玩一个情节(一个完整的游戏称为一个情节),然后在每个情节之后更新其 Q 值。智能体将不得不经历很多情节来充分探索环境。
# hyperparameters
learning_rate = 0.01
n_episodes = 100_000
start_epsilon = 1.0
epsilon_decay = start_epsilon / (n_episodes / 2) # reduce the exploration over time
final_epsilon = 0.1
env = gym.make('Blackjack-v1', natural=False, sab=False)
agent = BlackjackAgent(
env,
learning_rate=learning_rate,
initial_epsilon=start_epsilon,
epsilon_decay=epsilon_decay,
final_epsilon=final_epsilon,
)
备注
当前的超参数设置是为了快速训练一个像样的智能体。如果你想收敛到最优策略,可以尝试将 n_episodes
增加10倍,并降低学习率(例如,降到 0.001
)。
from tqdm import tqdm
env = gym.make("Blackjack-v1", sab=False)
env = gym.wrappers.RecordEpisodeStatistics(env, buffer_length=n_episodes)
for episode in tqdm(range(n_episodes)):
obs, info = env.reset()
done = False
# play one episode
while not done:
action = agent.get_action(obs)
next_obs, reward, terminated, truncated, info = env.step(action)
# update the agent
agent.update(obs, action, reward, terminated, next_obs)
# update if the environment is done and the current obs
done = terminated or truncated
obs = next_obs
agent.decay_epsilon()
0%| | 0/100000 [00:00<?, ?it/s]
100%|██████████| 100000/100000 [00:19<00:00, 5214.73it/s]