Courses

强化学习(RL)是机器学习生态中的一部分,智能体通过与环境交互来学习实现目标的最优策略。它与监督式机器学习算法截然不同,后者需要摄取并处理数据。强化学习不需要数据集,而是依据环境与奖励机制来学习,以便做出更好的决策。
例如,在马里奥视频游戏中,如果角色采取一个随机动作(如向左移动),基于该动作,它可能会获得一个奖励。执行动作后,智能体(马里奥)会进入一个新状态,如此循环,直到游戏角色到达关卡终点或死亡。
这样的回合会反复多次,直到马里奥通过最大化奖励学会在环境中导航。

图片由作者提供
我们可以将强化学习拆解为五个简单步骤:
- 智能体处于环境中的初始状态。
- 它会基于特定策略采取一个动作。
- 它将根据该动作获得奖励或惩罚。
- 通过从先前的移动中学习并优化策略。
- 重复该过程,直到找到最优策略。
阅读我们的教程强化学习简介以了解更多。您将通过代码示例进一步探索强化学习的工作原理。
在本教程中,我们将学习 Q-learning,并理解为何需要深度 Q-learning。此外,我们将学习如何使用 Numpy 和 Gymnasium 从零创建并训练 Q-learning 算法。
注意:如果您是机器学习新手,建议先学习我们的Python 机器学习科学家职业路径,以更好地理解强化学习和 Q-Learning。
什么是 Q-Learning?
Q-learning 是一种无模型、基于价值、离策略的算法,它会基于智能体的当前状态找到最佳的动作序列。“Q”代表质量(quality)。质量表示某个动作在最大化未来回报方面的价值。
基于模型的算法使用转移函数和奖励函数来估计最优策略并构建模型。相反,无模型算法通过经验学习其动作的后果,而不需要转移与奖励函数。
基于价值的方法训练价值函数,以学习哪个状态更有价值并据此采取动作。而基于策略的方法直接训练策略,以学习在给定状态下应采取的动作。
在离策略中,算法评估并更新的策略与执行动作所用的策略不同。相反,在策略算法评估并改进的是与执行动作相同的策略。
Q-learning 关键术语
在深入了解 Q-learning 的工作原理之前,我们需要先掌握一些有助于理解其基础的术语。
- 状态(s):智能体在环境中的当前位置。
- 动作(a):智能体在某一特定状态下采取的一步操作。
- 奖励:每个动作都会带来奖励或惩罚。
- 回合(Episode):阶段结束,智能体无法再采取新动作。发生在智能体达成目标或失败时。
- Q(St+1, a):在特定状态下执行某动作的期望最优 Q 值。
- Q(St, At):对 Q(St+1, a) 的当前估计。
- Q 表:智能体维护的状态与动作集合的 Q 值表。
- 时序差分(TD):使用当前与先前的状态和动作来估计 Q(St+1, a) 的期望值。
Q-Learning 如何工作?
我们将通过一个“冰湖”示例来详细学习 Q-learning 的工作方式。在该环境中,智能体必须从起点穿越冰湖到达目标,且不能掉入洞中。最佳策略是以最短路径到达目标。

动图由作者提供
Q 表
智能体将使用 Q 表,根据每个状态的期望奖励来采取最佳可能动作。通俗来说,Q 表是一种由动作与状态组成的数据结构,我们使用 Q-learning 算法来更新表中的数值。
Q 函数
Q 函数使用 Bellman 方程,并以状态(s)与动作(a)为输入。该方程简化了状态值与状态-动作值的计算。
图片来源于 freecodecamp.org
Q-learning 算法

图片由作者提供
初始化 Q 表
我们首先初始化 Q 表。列数基于动作数量,行数基于状态数量。
在我们的示例中,角色可以向上、下、左、右移动。我们有四个可能动作和四个状态(开始、停留、错误路径与结束)。您也可以将错误路径理解为掉入洞中。我们将 Q 表初始化为 0。

图片由作者提供
选择一个动作
第二步很简单。开始时,智能体会随机选择动作(向下或向右),第二次运行时,它将使用更新后的 Q 表来选择动作。
执行动作
选择并执行动作会重复多次,直到训练循环停止。第一次的动作与状态是使用 Q 表选择的。在我们的例子中,Q 表中的所有值都是零。
随后,智能体会向下移动,并使用 Bellman 方程更新 Q 表。每次移动时,我们都会更新 Q 表中的数值,并用它来决定最佳行动路线。
起初,智能体处于探索模式,选择随机动作来探索环境。Epsilon-Greedy 策略是一种平衡探索与利用的简单方法。epsilon 表示选择探索的概率;当探索概率较小的时候,则更偏向利用。
一开始,epsilon 较高,意味着智能体更倾向于探索。随着对环境的探索,epsilon 会降低,智能体开始利用环境中的已知信息。在探索过程中,随着每次迭代,智能体在估计 Q 值方面会越来越有信心。

图片由作者提供
在冰湖示例中,智能体对环境一无所知,因此它最初采取随机动作(向下移动)。如上图所示,Q 表已通过 Bellman 方程进行了更新。
度量奖励
采取动作后,我们将衡量结果与奖励。
- 到达目标的奖励为 +1
- 走错路(掉入洞中)的奖励为 0
- 停留或在冰面上移动的奖励也为 0。
更新 Q 表
我们将使用方程更新函数 Q(St, At)。它利用上一个回合的 Q 值估计、学习率以及时序差分误差。时序差分误差通过即时奖励、折扣后的最大期望未来回报以及先前的 Q 值估计来计算。
该过程会重复多次,直到 Q 表更新完成且 Q 值函数被最大化。

图片由作者提供 | 方程可视化来自 Thomas Simonini
在开始阶段,智能体通过探索环境来更新 Q 表。当 Q 表准备就绪后,智能体将开始利用并做出更优决策。
图片由作者提供
在冰湖问题中,智能体将学会以最短路径到达目标,并避免跳入洞中。
Q-Learning Python 教程
本节我们将使用 Gymnasium 环境、Pygame 和 Numpy 从零构建 Q-learning 模型。该 Python 教程改编自 Thomas Simonini 的Notebook。它涵盖环境与 Q 表初始化、定义贪心策略、设置超参数、创建与运行训练循环和评估,以及可视化结果。
如果您在创建和运行训练循环时遇到问题,可以查看带输出的代码源。
环境搭建
设置虚拟显示
我们将先安装所有依赖,以生成回放视频(Gif)。需要一块虚拟屏幕(pyvirtualdisplay)来渲染环境并记录帧。
注意:使用 %%capture 可以抑制 Jupyter 单元格的输出。
%%capture
!pip install pyglet==1.5.1
!apt install python-opengl
!apt install ffmpeg
!apt install xvfb
!pip3 install pyvirtualdisplay
# Virtual display
from pyvirtualdisplay import Display
virtual_display = Display(visible=0, size=(1400, 900))
virtual_display.start()
安装依赖
我们现在安装用于创建、运行和评估训练循环的依赖。
- gymnasium:用于初始化 FrozenLake-v1 环境。
- pygame:用于 FrozenLake-v1 的界面。
- numPy:用于创建和处理 Q 表。
%%capture
!pip install gymnasium
!pip install pygame
!pip install numpy
!pip install imageio imageio_ffmpeg
导入包
现在导入所需库。
- Imageio 用于创建动画。
- tqdm 用于进度条。
import numpy as np
import gymnasium as gym
import random
import imageio
from tqdm.notebook import trange
冰湖 Gymnasium 环境
我们将使用Frozen Lake gymnasium 库创建一个非滑 4x4 的环境。
- 有两个网格版本,“4x4”与“8x8”。
- 如果
is_slippery=True,由于冰面的滑性质,智能体可能不会按预期方向移动。
在初始化环境后,我们将进行环境分析。
env = gym.make("FrozenLake-v1",map_name="4x4",is_slippery=False)
print("Observation Space", env.observation_space)
print("Sample observation", env.observation_space.sample()) # display a random observation
环境中有 16 个唯一的空间,并以随机位置显示。
Observation Space Discrete(16)
Sample observation 15
让我们了解动作数量并显示一个随机动作。
动作空间:
- 0:向左移动
- 1:向下移动
- 2:向右移动
- 3:向上移动
奖励函数:
- 到达目标:+1
- 掉入洞中:0
- 停留在冰面:0
print("Action Space Shape", env.action_space.n)
print("Action Space Sample", env.action_space.sample())
Action Space Shape 4
Action Space Sample 1
创建并初始化 Q 表
Q 表的列是动作,行是状态。我们可以使用 Gymnasium 获取动作空间与状态空间,然后据此创建 Q 表。
state_space = env.observation_space.n
print("There are ", state_space, " possible states")
action_space = env.action_space.n
print("There are ", action_space, " possible actions")
There are 16 possible states
There are 4 possible actions
为了初始化 Q 表,我们将创建一个状态空间 × 动作空间大小的 Numpy 数组。这里创建一个 16 × 4 的数组。
def initialize_q_table(state_space, action_space):
Qtable = np.zeros((state_space, action_space))
return Qtable
Qtable_frozenlake = initialize_q_table(state_space, action_space)
Epsilon-Greedy 策略
上一节我们了解了处理探索与利用权衡的 Epsilon-Greedy 策略。以 1 - ɛ 的概率进行利用,以 ɛ 的概率进行探索。
在 epsilon_greedy_policy 中我们将:
- 生成 0 到 1 的随机数。
- 如果随机数大于 epsilon,我们将进行利用。意味着智能体会在给定状态下选择值最高的动作。
- 否则,我们将进行探索(采取随机动作)。
def epsilon_greedy_policy(Qtable, state, epsilon):
random_int = random.uniform(0,1)
if random_int > epsilon:
action = np.argmax(Qtable[state])
else:
action = env.action_space.sample()
return action
定义贪心策略
如前所述,Q-learning 是一种离策略算法,这意味着采取动作与更新函数所用的策略不同。
在本例中,Epsilon-Greedy 策略是行为策略,而贪心策略是更新策略。
当智能体训练完成后,贪心策略也将成为最终策略。它用于从 Q 表中选择具有最高状态-动作值的选项。
def greedy_policy(Qtable, state):
action = np.argmax(Qtable[state])
return action
模型超参数
这些超参数用于训练循环,微调它们将带来更好的结果。
智能体需要充分探索状态空间以学习良好的价值近似;因此需要逐步衰减 epsilon。如果衰减率过高,智能体可能会因探索不足而陷入局部最优。
- 有 10,000 个训练回合与 100 个评估回合。
- 学习率为 0.7。
- 我们使用 "FrozenLake-v1" 作为环境,每个回合的最大步数为 99。
- gamma(折扣率)为 0.95。
- eval_seed:环境的评估随机种子。
- 探索的epsilon 概率起始为 1.0,最小概率为 0.05。
- epsilon 概率的指数衰减率为 0.0005。
# Training parameters
n_training_episodes = 10000
learning_rate = 0.7
# Evaluation parameters
n_eval_episodes = 100
# Environment parameters
env_id = "FrozenLake-v1"
max_steps = 99
gamma = 0.95
eval_seed = []
# Exploration parameters
max_epsilon = 1.0
min_epsilon = 0.05
decay_rate = 0.0005
模型训练
在训练循环中,我们将:
- 为训练回合创建循环。
- 首先降低 epsilon。随着每个回合推进,我们需要减少探索、增加利用。
- 重置环境。
- 为最大步数创建嵌套循环。
- 使用 Epsilon-Greedy 策略选择动作。
- 采取动作(At),并观察期望奖励(Rt+1)与状态(St+1)。
- 采取动作(a),并观察结果状态(s')与奖励(r)。
- 使用公式更新 Q 函数。
- 如果
done= True,结束该回合并跳出循环。 - 最后,将当前状态更新为新状态。
- 完成所有训练回合后,函数将返回更新后的 Q 表。
def train(n_training_episodes, min_epsilon, max_epsilon, decay_rate, env, max_steps, Qtable):
for episode in trange(n_training_episodes):
epsilon = min_epsilon + (max_epsilon - min_epsilon)*np.exp(-decay_rate*episode)
# Reset the environment
state = env.reset()
step = 0
done = False
# repeat
for step in range(max_steps):
action = epsilon_greedy_policy(Qtable, state, epsilon)
new_state, reward, done, info = env.step(action)
Qtable[state][action] = Qtable[state][action] + learning_rate * (reward + gamma * np.max(Qtable[new_state]) - Qtable[state][action])
# If done, finish the episode
if done:
break
# Our state is the new state
state = new_state
return Qtable
我们用时 3 秒完成了 10,000 个训练回合。
Qtable_frozenlake = train(n_training_episodes, min_epsilon, max_epsilon, decay_rate, env, max_steps, Qtable_frozenlake)

如您所见,训练后的 Q 表已有数值,智能体现在将使用这些数值来导航环境并实现目标。
Qtable_frozenlake
array([[0.73509189, 0.77378094, 0.77378094, 0.73509189],
[0.73509189, 0. , 0.81450625, 0.77378094],
[0.77378094, 0.857375 , 0.77378094, 0.81450625],
[0.81450625, 0. , 0.77378094, 0.77378094],
[0.77378094, 0.81450625, 0. , 0.73509189],
[0. , 0. , 0. , 0. ],
[0. , 0.9025 , 0. , 0.81450625],
[0. , 0. , 0. , 0. ],
[0.81450625, 0. , 0.857375 , 0.77378094],
[0.81450625, 0.9025 , 0.9025 , 0. ],
[0.857375 , 0.95 , 0. , 0.857375 ],
[0. , 0. , 0. , 0. ],
[0. , 0. , 0. , 0. ],
[0. , 0.9025 , 0.95 , 0.857375 ],
[0.9025 , 0.95 , 1. , 0.9025 ],
[0. , 0. , 0. , 0. ]])
评估
evaluate_agent 会运行 n_eval_episodes 个回合,并返回奖励的均值和标准差。
- 在循环中,我们首先检查是否存在评估种子。如果没有,则不带种子重置环境。
- 嵌套循环将运行至 max_steps。
- 智能体会在给定状态下,使用 Q 表选择具有最大期望未来回报的动作。
- 计算回报。
- 更新状态。
- 若结束(智能体掉入洞中或已达到目标),则跳出循环。
- 追加结果。
- 最后,我们使用这些结果计算均值与标准差。
def evaluate_agent(env, max_steps, n_eval_episodes, Q, seed):
episode_rewards = []
for episode in range(n_eval_episodes):
if seed:
state = env.reset(seed=seed[episode])
else:
state = env.reset()
step = 0
done = False
total_rewards_ep = 0
for step in range(max_steps):
# Take the action (index) that have the maximum reward
action = np.argmax(Q[state][:])
new_state, reward, done, info = env.step(action)
total_rewards_ep += reward
if done:
break
state = new_state
episode_rewards.append(total_rewards_ep)
mean_reward = np.mean(episode_rewards)
std_reward = np.std(episode_rewards)
return mean_reward, std_reward
如您所见,我们获得了满分且标准差为零。这意味着我们的智能体在全部 100 个回合中都达到了目标。
# Evaluate our Agent
mean_reward, std_reward = evaluate_agent(env, max_steps, n_eval_episodes, Qtable_frozenlake, eval_seed)
print(f"Mean_reward={mean_reward:.2f} +/- {std_reward:.2f}")
Mean_reward=1.00 +/- 0.00
结果可视化
到目前为止我们一直在处理数字。为了演示效果,我们需要创建一个从开始到达成目标的智能体动画 Gif。
- 我们将首先通过使用 0-500 的随机整数重置环境来创建初始状态。
- 使用 rdb_array 渲染环境以创建图像数组。
- 将
img追加到images数组。 - 在循环中,我们将使用 Q 表进行每一步动作并渲染每一步的图像。
- 最后,我们将使用该数组及 imageio 每秒一帧地创建 Gif。
def record_video(env, Qtable, out_directory, fps=1):
images = []
done = False
state = env.reset(seed=random.randint(0,500))
img = env.render(mode='rgb_array')
images.append(img)
while not done:
# Take the action (index) that have the maximum expected future reward given that state
action = np.argmax(Qtable[state][:])
state, reward, done, info = env.step(action) # We directly put next_state = state for recording logic
img = env.render(mode='rgb_array')
images.append(img)
imageio.mimsave(out_directory, [np.array(img) for i, img in enumerate(images)], fps=fps)
如果您在 Jupyter 笔记本中,可以使用 IPython.display 的 Image 函数显示 Gif。
video_path="/content/replay.gif"
video_fps=1
record_video(env, Qtable_frozenlake, video_path, video_fps)
from IPython.display import Image
Image('./replay.gif')
现在,您可以将这些结果分享给同事与同学,或发布在社交媒体上。
Q-Learning 常见问题
Q-learning 的缺点是什么?
在 Q-learning 中,学习过程对智能体而言代价较高,尤其在初期。为什么?为了收敛到最优策略,需要频繁访问每一个状态-动作对。
为什么叫 Q-learning?
在 Q-learning 中,“Q”代表质量(quality)。它表示某个动作在实现未来回报方面的有用程度,用于创建状态与动作的映射体系,以最大化期望回报。
为什么 Q-Learning 是离策略?
在 Q-learning 中,更新策略与行为(动作)策略不同,这就是它被称为离策略(off-policy)算法的原因。
Q-learning 总会收敛吗?
是的。在训练过程中,该算法总是会收敛到最优策略。
为什么我们需要深度 Q-learning?
Q-learning 是为更小且离散的环境设计的简单算法。对于更大的环境,我们需要一个极其庞大的状态-动作 Q 表,训练会占用大量内存与算力。而深度 Q-learning 用神经网络替代 Q 表,以处理包含连续动作与状态的大规模环境。