本书讨论的是如何让 AI 智能体与人类意图保持一致。过去二十年间,AI 能力呈指数级提升,而机器学习、计算机视觉、自然语言处理、深度学习以及强化学习(Reinforcement Learning,RL)等领域的一系列里程碑,对这一跃升起到了关键作用。这些能力与应用展示了 AI 极强的可扩展性,也让它几乎影响到一切领域。
正因为如此,AI 的行为或其采取的行动必须与人类意图保持一致,这一点就显得极其重要;而这种“对齐”的必要性,本身也是安全使用这项技术的基础路径。
本书将深入探讨基于人类反馈的强化学习(Reinforcement Learning from Human Feedback,RLHF)。这项技术在生成式预训练模型的成功中扮演了关键角色,并推动它们进入主流技术应用场景。全书第一部分将从总体视角介绍 RLHF,帮助你建立将 RLHF 应用于任意场景所需的核心技术理解;第二部分将深入到语言建模,尤其是大语言模型中的 RLHF 应用;最后一部分则会讨论这项技术的当前趋势以及未来走向。
在本章中,我们会先介绍强化学习本身:先讲 RL 的核心基础,以及理解 RL 算法所需的关键概念,并从高层次解释其不同类别。随后,我们会引入一个经典且简单的环境——网格世界(gridworld),展示一个基础强化学习算法 Q-learning 是如何实现的。在此基础实现作为参照和背景的前提下,我们会进一步详细说明奖励函数如何设计,以及如何通过迁移学习加速 RL 训练。最后,我们还会展示人类反馈如何帮助基于 RL 的训练过程,并用同一个例子进行动手实现,让你能够清楚地对比它相对于经典 Q-learning 的改进效果。这样,你就能对 RLHF 在实践中如何工作形成直观认识,也为后续章节深入更复杂的算法与架构打下基础。
本章将覆盖以下主要内容:
- 强化学习基础
- 理解 RL 算法
- RL 环境导航——一个 gridworld 环境
- 奖励函数设计与迁移学习
- 基于人类反馈的强化学习
技术要求
本章中,我们尽量将所有与实现相关的依赖保持在最低限度。全部实现均使用 Python。推荐版本与最低依赖如下:
- Python 版本 >= 3.9
- Numpy == 1.26.4
- Matplotlib == 3.8.4
开始之前,请克隆本书配套的 Git 仓库,并切换到 chapter1 目录:
https://github.com/PacktPublishing/A-Practical-Guide-to-Reinforcement-Learning-from-Human-Feedback
强化学习基础
强化学习(RL)是机器学习的一种范式,关注的是:智能体如何通过与环境交互来学习决策,以最大化给定的累计奖励。
在人工智能语境中,智能体(agent) 是任何能够感知环境,并基于感知结果自主做出决策、进而对环境施加作用的实体。智能体通过“试错”进行学习:当它执行动作后,会因为到达某个状态、停留在某个状态,或者因动作带来的结果,而收到奖励或惩罚形式的反馈。随着这样的迭代过程不断进行,智能体会逐步打磨自己的决策能力,以实现累计奖励最大化。
RL 建立在状态—动作—奖励(state-action-reward)框架之上。智能体通过与环境交互,学习最优的决策策略。环境当前的状态会通过传感器传递给智能体,使其能够基于环境信息做出合理决策。状态(state) 表示环境的各种属性,是决策的重要依据。
例如,人类通过视觉等感官感知世界,而机器人可能通过摄像头获取信息。这些感知共同构成了状态,其中可能包括物体、位置以及其他环境属性。不过,并不是所有状态都能被直接观测到;有些隐藏对象虽然构成了当前状态的一部分,却并未被观测到。因此,观测(observation) 指的是智能体实际感知到的状态信息,也是其做决策时真正使用的信息。为了简洁起见,在本书后续讨论中,我们会将“状态”直接视作“观测”,假设观测中已经包含了做决策所需的所有相关状态信息。
设想一个机器人根据摄像头输入在环境中导航。它会解析图像,决定下一步向右、向左还是直行。这些动作通过肢体、轮子等执行机构实现。智能体会使用决策机制——从简单规则到复杂的机器学习算法——根据自己对环境的感知来选择动作。
在 RL 中,智能体的目标是通过选择能带来良好结果的动作,来最大化累计奖励。奖励(reward) 作为反馈信号,引导智能体的学习过程。通过反复交互,智能体会不断优化自己的决策能力,最终借助状态—动作—奖励框架,在不同环境中获得最优表现。
在图 1.1 中,我们展示了基于状态—动作—奖励框架的智能体—环境交互方式。
图 1.1——智能体与环境的交互
在某一给定的迭代步骤或时间点上,基于环境的初始状态 ,智能体会采取动作来改变环境,使其转移到状态 。在每一次迭代中,如果这种状态变化是我们所期望的,就会为其赋予较高的正向奖励,以激励智能体继续采取此类动作。
促使智能体在当前状态下采取动作 的决策策略,被称为策略(policy),通常记作 。
动作 会作为输入施加到环境之中,并促使环境改变其状态,或者转移到下一个状态 。这种状态转移机制通常表示为 。状态转移动态由环境本身决定,并不受我们或智能体控制。根据具体环境的不同,它可能是随机的,也可能是确定性的。
从初始状态出发,在遵循某一特定策略的前提下,对一系列状态上的期望累计奖励所作的估计,被称为价值函数(Value Function),通常记作 ,它是关于状态的函数。当这个价值函数对应的是在某个特定状态下采取某个特定动作时,它就被称为动作价值函数(Action Value Function),也就是 Q 函数。
理解 RL 算法
RL 算法的核心任务,是优化策略,使智能体能够采取最优动作,从而到达期望状态。
为了优化动作,RL 算法会采用各种不同的技术,但本质上都依赖一个共同机制:利用智能体处于某个状态时所对应的奖励,驱动策略产生更理想的动作。不同算法之间的差异,主要体现在:状态、动作和奖励是如何被使用、存储、处理并最终用于生成最优策略的,以及智能体与环境是如何交互以学习这些策略的。
RL 算法可以按多个维度分类,常见分类方式如下:
基于模型 vs 无模型
- 基于模型(Model-based) :会学习环境模型,并利用该模型规划动作。环境模型可以基于动态规划、蒙特卡洛方法,或通过学习状态转移模型构建。
- 无模型(Model-free) :不显式建模环境,而是直接通过与环境交互来学习。典型例子包括 Q-learning、SARSA 和 Deep Q Networks(DQN)。
基于价值 vs 基于策略
- 基于价值(Value-based) :估计某状态下执行某动作的价值,并尝试最大化价值函数。
- 基于策略(Policy-based) :直接参数化策略,并通过梯度上升等方法学习最优策略。
- 混合方法:结合价值方法与策略方法的典型代表是 Actor-Critic 算法。它同时维护一个 actor(策略)和一个 critic(价值函数),并利用两者的优势共同学习。
时序差分 vs 蒙特卡洛 vs 动态规划
-
时序差分学习(Temporal Difference, TD) :根据当前奖励与未来奖励之间的差异来更新价值函数。
-
蒙特卡洛(Monte Carlo) :根据整个 episode(一次完整运行中的状态序列)结束后观察到的总回报来估计价值,因此每个 episode 只更新一次价值。
由于必须等整个轨迹跑完之后才能更新价值,这一点也是蒙特卡洛方法的典型弱点。
-
动态规划(Dynamic Programming) :将问题拆分为子问题,并通过迭代方式求解。动态规划在已知环境、有限状态—动作空间下非常重要,也有助于算法设计;但它在计算复杂度、时间和内存需求上都难以扩展到大规模状态—动作空间,也不适合存在不确定性的环境。
On-policy vs Off-policy
- On-policy:一边学习策略,一边立刻按当前策略采取动作。
- Off-policy:学习的是目标策略,但执行动作时采用的是另一个行为策略(behavioral policy)。
单智能体 vs 多智能体
- 单智能体(Single-agent) :一个智能体在环境中独立交互并学习实现目标。
- 多智能体(Multi-agent) :多个智能体在同一环境中相互交互,可以协作、竞争,或独立地共同推动一个或多个目标的达成。此时智能体之间通常存在协调与通信,也可能是多个单智能体系统的集成。
表格型 RL vs 函数逼近型 RL
- 表格型 RL(Tabular RL) :显式地用表格存储价值和策略。
- 函数逼近 RL(Function Approximation RL) :使用神经网络等函数逼近器来估计价值函数或策略,从而更高效地处理大规模或连续状态—动作空间。
表格型 RL 非常适合理解 RL 算法、建立直觉或开发原型,但由于它需要以表格形式存储信息,因此只适合较小的状态—动作空间。
批量 RL vs 在线 RL
- 批量/离线 RL(Batch / Offline RL) :智能体从预先离线采集好的固定数据集中学习,在训练过程中不与环境交互。
- 在线 RL(Online RL) :智能体在与环境持续交互的过程中学习,并随着经验不断积累,持续更新策略或价值函数。
探索—利用策略
RL 算法还可以按智能体在训练过程中如何权衡**探索(exploration)与利用(exploitation)**来分类。
- 当智能体尝试新的动作、发现新情境,并了解新动作如何影响状态演化和最终奖励时,这称为探索。
- 当智能体优先选择那些已知可以带来高奖励的动作,而不再冒险尝试新动作时,这称为利用。
典型策略包括:
- epsilon-greedy
- Boltzmann exploration
- Upper Confidence Bound(UCB)
- Thompson Sampling
尽管 RL 算法的分类方式可以列得非常细,很多算法其实同时落在多个类别中。就 RLHF 而言,我们会重点关注一些关键基础算法。例如,本章将以 Q-learning 作为基于价值方法的代表;在后续章节中,我们还会引入基于策略的方法。
在进入完整示例之前,我们需要先理解价值函数的概念。价值函数用于评估环境中某个状态,或者某个状态—动作对的质量。它提供了一种量化方式,用来表示:在某个状态下,或在某个状态下执行某个动作并遵循给定策略时,预期可获得的回报或效用。这帮助我们判断某个状态(或在该状态下采取特定动作)在长期来看有多“好”。
价值函数通常有两种定义方式:
-
状态价值函数(State value function) :它表示从某个特定状态 sss 出发,并遵循某一给定策略时的期望回报,也就是累计折扣奖励。从数学上讲,未来折扣奖励的期望总和可以表示为:
。其中, 是期望算子,表示对所有可能未来轨迹的期望; 是折扣因子(取值介于 0 和 1 之间),用于对未来奖励进行折扣处理,从而让即时奖励具有更高权重;而 表示在时刻 收到的奖励。
-
动作价值函数(Action value function) :它表示从某个状态 出发,先采取动作 ,然后再遵循某一给定策略时的期望回报。从数学上讲,这一期望的未来折扣奖励总和可以表示为:
。其中使用的符号含义与状态价值函数中的记号相同。
从系统和架构的角度看,在基于 RLHF 的 AI 系统中,算法本身其实是可以替换的;我们也可以把“状态—动作”视为输入—输出变量,与 AI 系统其余部分进行交互。对于那些希望深入研究 RL 算法本身的读者,市面上已有不少理论和实践兼备的优秀资料(本章末尾会列出参考文献)。
为了帮助理解 AI 系统中的智能体—世界交互,我们接下来会使用一个经典的 grid-world 环境,展示 Q-learning 算法是如何工作的。随后,我们还会进一步说明奖励模型是如何介入,从而让学习过程更高效。
RL 环境导航——一个网格世界环境
在 RL 中,需要被优化的核心场景,就是智能体与环境之间的交互过程。为了优化这一过程,智能体必须学会采取最优动作,从而到达理想状态。而那些能够通向最优状态的动作序列,就构成了策略。
训练智能体当然可以直接在真实世界环境中进行,但现实环境往往存在危险、成本高昂,而且效率不高。要让智能体学到足够复杂的策略,往往需要数百万次迭代。这时,对真实环境进行仿真就变得非常有帮助。
虽然仿真环境无法百分之百还原现实世界,但在很多工业和现实问题中,只要它捕捉到了那些真正会影响状态转移的关键环境属性,就已经足够用了。当然,在某些场景下,仿真与现实之间仍然存在差距,这通常被称为 sim2real gap。这种差距在涉及物理规律和三维几何资产建模的仿真中尤为明显;而在文本、图像或那些部署后仍然运行在数字世界中的应用里,这种问题通常会小得多。
在本章中,我们会引入一个用网格表示现实世界的简化环境,也就是 grid-world。这个简化环境能帮助我们把注意力聚焦在 RL 核心概念上,而不必承受复杂领域所带来的额外认知负担。
这个环境由一个个网格单元组成。每个单元可以是可访问的,也可以是不可访问的,从而共同构成整个世界的布局。智能体位于网格中的某一个单元内,并且可以通过执行动作,从一个单元移动到另一个单元。
在状态—动作—奖励框架下,我们把这些概念在 grid-world 问题中具体化如下:
- 状态(States) :网格中的每个单元都表示环境的一个状态。随着智能体在网格中移动,环境状态也随之变化。
- 动作(Actions) :智能体可以通过动作在网格中移动。通常包括向上、向下、向左、向右四种动作。但根据问题设定不同,某些动作可能会因为边界或障碍物而不可用。
- 奖励(Rewards) :网格中的每个单元都可以关联一个奖励值。智能体的目标通常是在时间维度上最大化总奖励。奖励可以是正的、负的,也可以是零。
- 终止状态(Terminal states) :有些单元会被指定为终止状态。当智能体进入终止状态时,一个 episode 就结束,且根据问题设定,它可能会收到最终奖励,也可能不会。
- 策略(Policy) :策略决定了智能体在环境中的行为。它将状态映射为动作,指导智能体在不同状态下如何选择动作以实现目标,而目标通常是最大化累计奖励。
- 转移动态(Transition dynamics) :从一个状态移动到另一个状态未必总是确定性的。环境中可能存在随机性,例如风把智能体吹偏,或冰面导致它打滑。
为了用 Python 构建这个 grid-world,我们首先创建一个 GridWorld 类。下面是构建该环境的步骤。
初始化环境
__init__ 方法负责初始化 grid-world 环境的各种参数,包括网格大小、起点与终点位置、障碍物位置等。状态总数初始化为网格大小,同时还会将每个状态—动作对对应的 Q 值初始化为 0:
def __init__(self, grid_size, obstacle_positions):
self.grid_size = grid_size
self.start_position = (2, 2)
self.goal_position = (5,5) #(grid_size[0] - 1,
# grid_size[1] - 1)
self.obstacle_positions = obstacle_positions
# right, left, down, up:
self.actions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
# Action symbols for visualization:
self.action_symbols = ['→', '←', '↓', '↑']
self.num_actions = len(self.actions)
self.num_states = np.prod(self.grid_size)
self.Q_values = np.zeros((self.num_states,
self.num_actions))
构建模拟器所需的方法
接下来,我们要添加一些方法,用于构建 grid-world 模拟器。下面两个方法分别用于在状态 (i, j) 与 Q 值数组中的索引之间进行相互转换:
def state_to_index(self, state):
return state[0] * self.grid_size[1] + state[1]
def index_to_state(self, index):
return (index // self.grid_size[1],
index % self.grid_size[1])
state_to_index(state) 方法说明
这个方法用于把一个状态(也就是网格中的位置元组)转换为 Q 值数组中的线性索引。我们拆开来看:
state[0]:表示该状态在网格中的行号self.grid_size[1]:表示网格的列数。因为grid_size是一个(rows, columns)形式的元组,所以self.grid_size[1]表示列数state[1]:表示该状态在网格中的列号
当你把 state[0] 乘以 self.grid_size[1] 时,本质上是在把二维网格中的“行号”展开成一维数组中的偏移量。例如,如果网格大小是 (6, 6),当前位置是 (2, 3),那么计算结果为:
2 * 6 + 3 = 15
这就把二维坐标 (2, 3) 映射成了一维索引 15。这样再加上列号,就能够把每一个二维坐标唯一映射到 Q 值数组中的一个位置,从而便于在 Q-learning 过程中高效地存取 Q 值。
index_to_state(index) 方法说明
这个方法的作用正好相反:它将 Q 值数组中的一个线性索引,转换回 grid 中对应的二维状态坐标 (i, j)。
index // self.grid_size[1]:通过整数除法得到行号,即在该索引之前完整包含了多少行index % self.grid_size[1]:通过取余得到列号
例如,假设索引 index = 20,网格大小仍为 (6, 6):
row_index = 20 // 6 = 3column_index = 20 % 6 = 2
因此,这个索引对应的状态就是 (3, 2)。
这个方法在需要把线性索引映射回二维坐标、进而解释或可视化 Q 值与策略时非常有用。
检查状态是否合法
由于我们的 grid-world 中包含障碍物,因此智能体所在的位置必须同时满足两个条件:
- 处于网格边界之内
- 不能与障碍物重合
为此,我们实现了 is_valid_state 方法,用于检查某个状态是否合法:
def is_valid_state(self, state):
return 0 <= state[0] < self.grid_size[0] \
and 0 <= state[1] < self.grid_size[1] \
and state not in self.obstacle_positions
推进到下一个状态
为了让智能体从当前状态转移到下一状态,我们实现 get_next_state() 方法。该方法接收当前状态和某个动作,并计算下一个状态。如果该动作会导致进入非法状态,那么智能体就停留在当前状态不动。
def get_next_state(self, state, action):
next_state = (state[0] + action[0],
state[1] + action[1])
if self.is_valid_state(next_state):
return next_state
return state
表达式 (state[0] + action[0], state[1] + action[1]) 的含义是:
state[0] + action[0]:得到新的行号state[1] + action[1]:得到新的列号
随后,该方法会使用 is_valid_state 检查新状态是否合法。如果合法,就返回这个新状态;否则返回当前状态,表示智能体无法移动过去,只能停留原地。
这个方法会在 Q-learning 算法内部被频繁调用,用来根据当前状态和所选动作决定接下来会到达哪个状态。它保证了智能体的移动始终遵守网格边界和障碍物约束。
Q-learning
前面这些方法完成了 grid-world 环境、状态、动作以及状态更新方式的定义。这就把“环境”这一侧搭建好了,也为智能体与环境交互提供了基础媒介。
接下来,为了让智能体能够利用这些状态信息并学习出一套动作策略,我们要实现 Q-learning 算法。
首先,算法会在多个 episode 上迭代,以探索不同的策略组合。每个 episode 开始时,会把当前状态设为起始位置,同时初始化奖励和步数。然后进入 episode 内部循环,只要当前状态还没有到达目标状态,智能体就会持续执行动作并改变状态。
为了学习环境,这里采用的是探索—利用策略中的 epsilon-greedy 方法。也就是说:
- 以较小概率
epsilon,智能体会随机选择动作(探索) - 在剩余的大多数情况下,它会选择当前 Q 值最大的动作(利用)
每走一步,都会给予 -1 的奖励;如果下一步到达目标状态,则给予较高奖励 10;如果下一步与障碍物位置重叠,则给予 -5 的惩罚,以抑制这种行为。
然后会计算一个目标值:
Target = r + gamma * Qmax
它表示“即时奖励 + 下一状态最大 Q 值的折扣和”。这个目标值用于更新当前状态—动作对的 Q 值。Q 值表示:在状态 s 下执行动作 a 所能带来的未来累计奖励期望。更新公式为:
Q_values = Q_t + learning_rate * (target – Q_t)
在每一次迭代中,总奖励、下一状态 s_t+1 以及步数等信息都会被更新,如下所示。
Q_learning 方法会根据收到的奖励来更新 Q 值。它会在指定数量的 episode 中不断迭代,每个 episode 都会经历若干状态转移,直到到达目标状态。动作选择采用 epsilon-greedy 策略,因此有时会执行次优动作,但学习到的 Q 值会逐步沉淀下来,反映长期收益。
下面是使用 Python 实现 Q-learning 的具体步骤。
函数定义与初始化
这一部分定义 q_learning 函数,并初始化所需变量:
rewards:记录每个 episode 的总奖励steps_per_episode:记录每个 episode 的步数policy_progress:在若干检查点记录策略的变化过程
如果没有提供 policy_checkpoints,则使用默认值:
def q_learning(self, num_episodes=1000, learning_rate=0.1,
discount_factor=0.9, epsilon=0.1, policy_checkpoints=None):
rewards = []
steps_per_episode = []
policy_progress = [] # Store optimal policy at different checkpoints
if policy_checkpoints is None:
policy_checkpoints = [
num_episodes // 1000, num_episodes // 100,
num_episodes // 50, num_episodes – 1
]
Episode 循环
接下来进入 episode 主循环。对于每个 episode:
- 将状态初始化为起始位置
- 初始化总奖励与步数
- 创建一个列表来记录访问过的状态
for episode in range(1, num_episodes+1):
state = self.start_position
total_reward = 0
steps = 0
visited_states = [state]
动作选择
这一段实现 epsilon-greedy 动作选择机制:
- 以
epsilon的概率随机选一个动作(探索) - 以
1 - epsilon的概率选取当前 Q 值最大的动作(利用)
while state != self.goal_position:
if np.random.rand() < epsilon:
action = np.random.choice(self.num_actions)
else:
action = np.argmax(self.Q_values[
self.state_to_index(state)])
状态转移与奖励分配
这一部分首先基于当前状态和所选动作计算下一状态,然后根据结果分配奖励:
- 到达目标:奖励
10 - 撞上障碍:奖励
-5 - 普通移动:默认奖励
-1
next_state = self.get_next_state(
state, self.actions[action])
reward = -1
if next_state == self.goal_position:
reward = 10
elif next_state in self.obstacle_positions:
reward = -5
更新 Q 值
接下来,根据 Q-learning 的更新规则,更新当前状态—动作对的 Q 值:
target = reward + discount_factor * np.max(
self.Q_values[self.state_to_index(next_state)])
self.Q_values[
self.state_to_index(state), action
] += learning_rate * (
target - self.Q_values[
self.state_to_index(state), action
])
这里的 target 是:
- 当前即时奖励
- 加上下一状态的最大未来收益(经过折扣因子衰减)
更新目标就是让当前 Q 值逐渐逼近这个 target。
状态更新与轨迹记录
更新完 Q 值之后,还要更新当前 episode 的统计信息:
- 将奖励累加到总奖励中
- 将状态推进到下一状态
- 步数加一
- 记录访问过的状态
total_reward += reward
state = next_state
steps += 1
visited_states.append(state)
记录奖励与步数
每个 episode 结束后,把该 episode 的总奖励和总步数分别存入列表,便于后续分析:
rewards.append(total_reward)
steps_per_episode.append(steps)
策略检查点记录
在指定的 checkpoint 上,保存当前策略,以便后续可视化训练过程中的策略变化。同时,每 20 个 episode 打印一次训练进度,方便监控:
if episode in policy_checkpoints:
policy_progress.append((
self.get_optimal_policy(),
visited_states, episode
))
if episode % 20 == 0:
print(f"Episode {episode}/{num_episodes}, "
f"Total Reward: {total_reward}, "
f"Steps: {steps}")
返回结果
全部 episode 完成后,返回收集到的奖励、步数以及策略演化记录:
return rewards, steps_per_episode, policy_progress
跟踪最优策略
为了支持日志记录和策略检查点保存,我们还需要添加一个 get_optimal_policy 方法。它会为网格中的每一个单元格分配一个“当前最优动作”,这个动作由该状态下 Q 值最大的动作决定;如果该状态是目标位置或障碍物位置,则将其标记为 -1,表示不存在有效动作。
下面是该方法的实现。
首先,定义函数,并初始化一个与网格维度相同的矩阵 optimal_policy,用来存储每个状态对应的最优动作:
def get_optimal_policy(self):
optimal_policy = np.zeros(self.grid_size, dtype=int)
然后,函数会遍历网格中的每个单元,把每个单元都看作一个状态:
for i in range(self.grid_size[0]):
for j in range(self.grid_size[1]):
state = (i, j)
接着,它会先判断当前状态是否是目标状态或障碍物状态。如果是,就把该位置在 optimal_policy 中对应的值设为 -1,表示这些状态不需要再采取动作:
if state == self.goal_position:
optimal_policy[i, j] = -1
elif state in self.obstacle_positions:
optimal_policy[i, j] = -1
若既不是目标状态也不是障碍物状态,则需要进一步找出当前状态下有哪些动作是合法的。函数会把这些动作收集到 valid_actions 列表中。它会遍历所有可能动作,计算动作执行后的下一状态,并判断该下一状态是否合法:
else:
valid_actions = []
for action in range(self.num_actions):
next_state = self.get_next_state(
state, self.actions[action])
if self.is_valid_state(next_state):
valid_actions.append(action)
最后,在所有合法动作中,找出 Q 值最大的那个动作,把它作为当前状态的最优动作。如果没有合法动作,则设为 -1:
if valid_actions:
optimal_policy[i, j] = np.argmax([
self.Q_values[
self.state_to_index(state), a
]
for a in valid_actions])
else:
optimal_policy[i, j] = -1 # No valid action
return optimal_policy
这个函数能够在考虑目标状态与障碍物约束的前提下,高效地为一个网格环境计算当前最优策略。
可视化与训练进度跟踪
为了跟踪并可视化学习进展,我们还加入了三个绘图方法,分别用于展示:
- 每个 episode 的奖励变化
- 每个 episode 智能体到达目标所需步数
- 策略的演化过程
奖励—Episode 曲线
下面的代码用于绘制“每个 episode 的总奖励”曲线,从而展示 Q-learning 的整体学习表现:
def plot_rewards(self, rewards):
plt.plot(rewards)
plt.xlabel('Episode')
plt.ylabel('Total Reward')
plt.title('Q-learning Performance')
plt.show()
图 1.2 展示了 Q-learning 在全部训练 episode 上的表现。横轴表示 episode,纵轴表示该 episode 的总奖励。曲线最开始的快速上升说明:大约在 50 个 episode 左右,策略就开始收敛。
图 1.2——1,000 个 episode 上的总奖励变化,展示了学习过程与性能表现
每个 Episode 的步数
为了观察智能体在训练过程中是否越来越高效,我们还可以绘制它每个 episode 为了到达目标所走的步数。实现如下:
def plot_steps_per_episode(self, steps_per_episode):
plt.plot(steps_per_episode)
plt.xlabel('Episode')
plt.ylabel('Steps')
plt.title('Steps per Episode')
plt.show()
plt.tight_layout()
图 1.3 展示了每个 episode 中到达目标所需步数的变化。最初,智能体可能要走多达 200 步才能到达目标;而当策略收敛之后,它所需步数也会收敛到 10 步以内。
图 1.3——grid-world 环境中,智能体每个 episode 到达目标所需步数随训练过程的变化
可视化策略演进
为了更直观地观察智能体如何一步步走向目标,我们实现了 plot_policy_progress 方法。这个方法会把整个 grid-world 绘制出来,并在图上标出智能体走过的完整轨迹。理论上,可以为每个 episode 都画一张轨迹图,但实际上只看几个关键 checkpoint 就足够观察策略演进了。
首先,函数会根据 policy_progress 中记录的 checkpoint 数量,创建对应数量的子图:
def plot_policy_progress(self, policy_progress):
num_checkpoints = len(policy_progress)
fig, axes = plt.subplots(
1, num_checkpoints,
figsize=(6*num_checkpoints, 6)
)
然后,逐个处理每个 checkpoint,并为每张子图设置标题,注明当前对应的 episode:
for i, (
policy, visited_states, episode
) in enumerate(policy_progress):
ax = axes[i]
ax.set_title(f'Policy at Episode {episode}')
接下来,函数会遍历策略网格中的每个状态,根据状态类型设置可视化表示:
- 目标位置:标记为
G - 障碍物:标记为
X - 起始位置:标记为
S - 其他位置:显示该位置下策略建议的动作方向
for y in range(policy.shape[0]):
for x in range(policy.shape[1]):
state = (y, x)
if state == self.goal_position:
ax.text(
x, self.grid_size[0] - y - 1, 'G',
va='center', ha='center', fontsize=20
)
ax.add_patch(plt.Rectangle((
x - 0.5,
self.grid_size[0] - y - 1 - 0.5
), 1, 1, color='lightgreen'))
elif state in self.obstacle_positions:
ax.text(
x, self.grid_size[0] - y - 1, 'X',
va='center', ha='center', fontsize=20
)
ax.add_patch(plt.Rectangle((
x - 0.5,
self.grid_size[0] - y - 1 - 0.5
), 1, 1, color='lightgray'))
elif state == self.start_position:
ax.text(
x, self.grid_size[0] - y - 1, 'S',
va='center', ha='center',
fontsize=20, color='red'
)
ax.add_patch(plt.Rectangle((
x - 0.5,
self.grid_size[0] - y - 1 - 0.5
), 1, 1, color='lightblue'))
else:
action = policy[y, x]
ax.text(
x, self.grid_size[0] - y - 1,
self.action_symbols[action],
va='center', ha='center', fontsize=20
)
最后,函数会把智能体在该 checkpoint 下走过的轨迹画出来,并为图像添加网格线与标签,使图形更易读:
for j in range(len(visited_states) - 1):
x1, y1 = visited_states[j][1], \
self.grid_size[0] - visited_states[j][0] - 1
x2, y2 = visited_states[j + 1][1], \
self.grid_size[0] - visited_states[j+1][0]-1
ax.plot([x1, x2], [y1, y2],
color='blue', linewidth=3)
ax.set_xticks(np.arange(
-0.5, policy.shape[1], 1))
ax.set_yticks(np.arange(
-0.5, policy.shape[0], 1))
ax.grid(
color='black', linestyle='-', linewidth=1)
ax.set_xticklabels([])
ax.set_yticklabels([])
ax.set_xlabel('X')
ax.set_ylabel('Y')
图 1.4 展示了策略在训练过程中的演化,以及智能体在若干代表性 episode 下所走的轨迹。这里记录了训练 1,000 个 episode 之后,第 1、10、20 和 999 个 episode 的快照:
- 在第 1 个 episode 中,智能体还在四处试探,并且会反复走回头路,因此轨迹较长
- 到第 10 个 episode 时,已经能看出一定进步
- 到第 20 个 episode 时,轨迹有明显优化
- 到第 999 个 episode 时,智能体已经学会了最优路径
你也可以尝试把 Q-learning 的训练 episode 数改成 200 或 500,并调整 checkpoint 数量,观察会发生什么变化。
图 1.4——训练过程中四个代表性 episode 下策略的演进情况
下面是实例化 GridWorld 类并运行 Q-learning 训练的执行代码:
# Adjust hyperparameters for experimentation
obstacle_positions = [(2, 3), (3, 1), (4, 3)]
grid_world = GridWorld(grid_size=(6, 6), obstacle_positions=obstacle_positions)
rewards, steps_per_episode, policy_progress = grid_world.q_learning(
num_episodes=1000, learning_rate=0.1,
discount_factor=0.95, epsilon=0.1
)
在上述 Q-learning 实现中,智能体已经能够在有限数量的 episode 内学会如何到达目标。前面介绍的几个可视化方法则帮助我们观察这一学习过程。
在这个例子中,我们使用的是一个 6 x 6 的网格,以及 3 个障碍物。对于这个问题,训练大约在 50 个 episode 左右即可收敛;当然,如果网格更大或障碍物更多,所需训练轮数也会相应增加。
要注意的是,GridWorld 类允许对网格大小和障碍物位置进行参数化设置,因此很方便用来修改环境。后续章节中,我们会继续利用这种参数化能力,探索进一步提升学习效率的策略。
奖励函数设计与迁移学习
为了让智能体学出我们希望的策略,在前面介绍的 Q-learning 算法中,我们通过正奖励鼓励期望行为,通过负奖励惩罚不希望出现的行为。
在 grid-world 问题中:
- 智能体每走一步,都会得到
-1的奖励,这样做是为了抑制它无意义地走太多步 - 如果某一步撞上障碍物,则给予
-5的较大负奖励,以帮助它学会避开障碍 - 如果到达目标位置,则给予
10的较大正奖励,从而鼓励它尽快抵达终点
这一套奖励分配逻辑,本质上就是奖励函数。代码如下:
reward = -1
if next_state == self.goal_position:
reward = 10
elif next_state in self.obstacle_positions:
reward = -5
需要注意的是:这个奖励函数是离散的,对于 grid-world 这种问题来说效果不错。但在某些场景下,奖励函数可能需要设计成连续的。
例如,如果网格非常大,假设达到 1000 x 1000,而其中只有少量障碍物,那么如果除了到达目标之外,其余情况全部都是 -1,智能体可能会在很长时间里毫无方向地游走。此时,如果能够随着它逐步靠近目标而给予奖励上的渐进提升,就会更有助于学习。一个可能的改进版本如下:
reward = -1 + np.exp(-0.1 * np.linalg.norm(
np.array(state) - np.array(self.goal_position)))
if next_state in self.obstacle_positions:
reward = reward – 5
其中这一行:
reward = -1 + np.exp(-0.1 * np.linalg.norm(
np.array(state) - np.array(self.goal_position)))
是在计算当前状态的奖励。它以 -1 为基础奖励,然后根据当前状态与目标位置之间的距离,对这个基础值进行调整。
这里的距离通过 NumPy 实现的欧几里得距离来计算:
np.linalg.norm(np.array(state) - np.array(self.goal_position))
然后,这个距离乘上 -0.1,再输入指数函数 np.exp:
- 乘以负值,意味着距离越远,奖励越低
- 使用指数函数,则意味着这个衰减过程是平滑渐进的,而不是线性的
随后,如果下一状态落在障碍物位置上,就再额外减去 5。这种惩罚会让智能体更倾向于避开障碍,因为走进障碍意味着更大的负收益。
迁移学习
在 RL 语境下,迁移学习(Transfer Learning, TL) 的含义是:把在一个任务上获得的知识,用来改善另一个相关但不同任务上的学习效率或性能。
为了在 grid-world 问题中演示迁移学习,我们会修改上一节介绍过的 q_learning 代码,并在类中增加几个新方法。
q_learning_transfer
这个方法用于迁移学习。它会把一个较小 grid-world(例如 6x6)中已经学到的 Q 值,迁移到一个更大的 grid-world(例如 8x8)中。然后,在新环境中基于这些迁移过来的 Q 值继续使用 Q-learning 进行微调:
def q_learning_transfer(self, pre_learned_Q_values,epsilon=0.01):
# Initialize Q-values with pre-learned values
self.Q_values[
:pre_learned_Q_values.shape[0],
:pre_learned_Q_values.shape[1]
] = pre_learned_Q_values
print(f"Q learning with Transferred Q-values")
# Perform Q-learning in the new environment
rewards_transfer, steps_per_episode, policy_progress = \
self.q_learning(epsilon=0.01)
return rewards_transfer, steps_per_episode, policy_progress
q_learning_scratch
这个方法表示“从零开始训练”。它会先重新把 Q 值初始化为全 0,然后在新环境中直接运行 Q-learning:
def q_learning_scratch(self):
self.Q_values = np.zeros((
self.num_states, self.num_actions
)) # Or initialize with other initial values if needed
print(f"Q learning from Scratch")
rewards_scratch, steps_per_episode, policy_progress = \
self.q_learning()
return rewards_scratch, steps_per_episode, policy_progress
对比迁移学习与从零训练
在分别完成迁移学习训练与从零训练之后,我们可以对比两者的效果。下面这个 plot_rewards_comparison 方法,就是用来比较两种方式的训练表现。
它首先会分别计算两种情况下的收敛速度:即奖励首次达到该情形下最大奖励 90% 时所对应的 episode:
def plot_rewards_comparison(
self, rewards_transfer, rewards_scratch
):
convergence_speed_transfer = next(
(
i
for i, reward in enumerate(rewards_transfer)
if reward >= 0.9 * max(rewards_transfer)
),
len(rewards_transfer))
convergence_speed_scratch = next(
(
i
for i, reward in enumerate(rewards_scratch)
if reward >= 0.9 * max(rewards_scratch)
),
len(rewards_scratch)
)
接着,它会分别累加两种训练方式在所有 episode 中获得的总奖励:
total_reward_transfer = sum(rewards_transfer)
total_reward_scratch = sum(rewards_scratch)
为了让曲线更平滑、更容易观察趋势,它使用窗口大小为 10 的移动平均进行平滑处理:
window_size = 10
rewards_transfer_smoothed = np.convolve(
rewards_transfer,
np.ones(window_size)/window_size, mode='valid'
)
rewards_scratch_smoothed = np.convolve(
rewards_scratch,
np.ones(window_size)/window_size, mode='valid'
)
之后,它会把两条平滑后的奖励曲线绘制出来,并使用不同样式和颜色区分,便于对比:
plt.plot(
np.arange(
window_size//2,
len(rewards_transfer_smoothed)
+ window_size//2
),
rewards_transfer_smoothed,
label='Transfer Learning',
linestyle='--', color='blue', alpha=0.7
)
plt.plot(
np.arange(
window_size//2,
len(rewards_scratch_smoothed)
+ window_size//2
),
rewards_scratch_smoothed,
label='Training from Scratch',
linestyle='--', color='orange', alpha=0.7
)
然后,它还会在图中用竖线标出若干与迁移学习相关的策略检查点:
transfer_epochs = [
pisode for _, _, episode in policy_progress]
for epoch in transfer_epochs:
plt.axvline(
x=epoch, color='gray',
linestyle='--', linewidth=0.8
)
为了让图更具可读性,还会额外在图中标注两种训练方式的收敛速度和总奖励:
plt.text(
0.5, 0.6,
f"Convergence Speed (Transfer): {convergence_speed_transfer}",
transform=plt.gca().transAxes)
plt.text(
0.5, 0.55,
f"Total Reward (Transfer): {total_reward_transfer}",
transform=plt.gca().transAxes)
plt.text(
0.5, 0.5,
f"Convergence Speed (Scratch): {convergence_speed_scratch}",
transform=plt.gca().transAxes)
plt.text(
0.5, 0.45,
f"Total Reward (Scratch): {total_reward_scratch}",
transform=plt.gca().transAxes
)
最后,为图表设置横轴、纵轴、标题和图例,然后显示:
plt.xlabel('Episode')
plt.ylabel('Total Reward')
plt.title('Q-learning Performance Comparison')
plt.legend()
plt.show()
下面这个 plot_comparison_metrics 方法则进一步从两个维度进行比较:
- 总奖励
- 每个 episode 到达目标所需步数
def plot_comparison_metrics(
self, rewards_transfer, rewards_scratch,
steps_transfer, steps_scratch
):
fig, axes = plt.subplots(1, 2, figsize=(12, 6))
ax1 = axes[0]
ax1.plot(rewards_transfer, label='Transfer Learning')
ax1.plot(rewards_scratch, label='Training from Scratch')
ax1.set_xlabel('Episode')
ax1.set_ylabel('Total Reward')
ax1.set_title('Total Reward Comparison')
ax1.legend()
ax2 = axes[1]
ax2.plot(steps_transfer, label='Transfer Learning')
ax2.plot(steps_scratch, label='Training from Scratch')
ax2.set_xlabel('Episode')
ax2.set_ylabel('Steps per Episode')
ax2.set_title('Steps per Episode Comparison')
ax2.legend()
plt.tight_layout()
plt.show()
下面给出一个完整的执行示例,用来展示策略训练与迁移学习的流程。
首先,我们实例化修改后的 GridWorld 类,创建一个 6x6 的 grid-world,并加入 3 个障碍物,用它训练出一套策略。运行 q_learning() 方法之后,就能得到该 6x6 环境下学出来的最优策略和 Q 值。后续迁移学习将直接使用这组 Q 值:
obstacle_positions = [(2, 3), (3, 1), (4, 3)]
grid_world = GridWorld(
grid_size=(6, 6), obstacle_positions=obstacle_positions)
rewards, steps_per_episode_6x6, policy_progress_6x6 = \
grid_world.q_learning()
grid_world.plot_policy_progress(policy_progress_6x6)
在图 1.5 中,可以看到这个 6x6 环境下 Q-learning 的策略演进过程。图中选取了第 6、20 和 100 个 episode 作为代表:
- 在第 6 和第 20 个 episode 时,智能体还在学习策略
- 到第 100 个 episode 时,它已经学会了通向目标的最优路径
图 1.5——6x6 网格中智能体在若干训练阶段的学习进展与轨迹
接下来,为了演示迁移学习,我们把这个 6x6 环境中训练得到的 Q 值,迁移到一个更复杂但相似的新环境:8x8 的网格,障碍物增加到 5 个。然后分别比较两种训练方式:
- 使用迁移过来的 Q 值继续训练
- 在这个新环境中从零训练
注意,这里不是拿 8x8 上的迁移学习直接去和 6x6 上的从零训练比较,而是都在 8x8 环境下比较。代码如下:
obstacle_positions_5 = [(2, 3), (3, 1), (4, 3), (5, 2), (2, 5)]
new_grid_world = GridWorld(
grid_size=(8,8), obstacle_positions=obstacle_positions_5)
rewards_transfer, steps_per_episode_transfer, policy_progress_transfer = \
new_grid_world.q_learning_transfer(grid_world.Q_values)
new_grid_world.plot_policy_progress(policy_progress_transfer)
rewards_scratch, steps_per_episode, policy_progress = \
new_grid_world.q_learning_scratch()
new_grid_world.plot_policy_progress(policy_progress)
图 1.6 展示了在 8x8 网格中,智能体借助从 6x6 网格迁移而来的 Q 值进行训练时的策略演进。可以看到:
- 第 6 个 episode 时,智能体仍在学习
- 到第 20 个 episode 时,它已经学会了到达目标的最优路径
- 到第 100 个 episode 时,它甚至会去探索另一条同样最优的路径
图 1.6——在 8x8 网格中,使用从 6x6 + 3 个障碍物环境迁移来的 Q 值后,智能体在若干 episode 下的轨迹
图 1.7 则展示了在同样的 8x8、5 个障碍物环境中,从零训练时的策略演进:
- 第 6 个 episode 仍处于摸索阶段
- 第 20 个 episode 有一定改善
- 到第 100 个 episode,智能体才真正学会最优路径
图 1.7——8x8 网格中从零训练时,智能体在若干 episode 下的轨迹
为了更系统地比较迁移学习与从零训练的差异,我们还会执行以下比较代码:
grid_world.plot_rewards_comparison(rewards_transfer, rewards_scratch)
grid_world.plot_comparison_metrics(
rewards_transfer, rewards_scratch,
steps_per_episode_transfer,steps_per_episode
)
图 1.8 展示了在 8x8 环境中,使用迁移学习与从零训练时,总奖励(经过窗口大小为 10 的平滑处理)随 episode 变化的对比结果。从图中可以看出,迁移学习显著加快了训练收敛速度:
- 使用迁移学习时,大约 7 个 episode 内就达到了奖励收敛
- 从零训练则需要大约 45 个 episode
同时,总奖励表现也明显更好:
- 迁移学习:
109 - 从零训练:
-1446
需要注意的是,图中并未绘制 episode 0 对应的初始策略;并且此时智能体仍然在使用 epsilon-greedy 策略,也就是说,训练过程中仍然存在探索行为。
图 1.8——总奖励随 episode 变化的平滑曲线,对比迁移学习(深色/蓝色)与从零训练
图 1.9 则进一步从两个角度分析:
- 左图:每个 episode 的原始总奖励
- 右图:每个 episode 中,智能体到达目标所需的步数
结果表明,迁移学习不仅更快收敛,而且在训练早期就能以更少步数到达目标。
图 1.9——迁移学习与从零训练在“原始每轮总奖励”和“每轮到达目标所需步数”上的对比
如果你运行上述代码,还会每隔 5 个 step 打印一次训练指标。下面是一段示例输出(中间部分使用 … 省略):
Q learning from Scratch
Episode 5/200, Total Reward: -116, Steps: 127…
Episode 15/200, Total Reward: -142, Steps: 153
Episode 20/200, Total Reward: 0, Steps: 11 …
Episode 100/200, Total Reward: 5, Steps: 6 …
Q learning with Transferred Q-values
Episode 5/200, Total Reward: -66, Steps: 77
Episode 10/200, Total Reward: 5, Steps: 6
Episode 15/200, Total Reward: 1, Steps: 10
Episode 20/200, Total Reward: 5, Steps: 6 …
从这些日志也能看出:使用迁移学习的智能体,在 10 个 episode 以内就已经能拿到正奖励;而从零训练则至少需要两倍以上的 episode 才能达到这个水平。
既然迁移学习已经展现出如此明显的加速效果,那么我们自然也有理由进一步乐观地期待:如果把人类反馈引入训练过程,会不会带来更显著的收益? 接下来这一节,我们就来验证这一点。
基于人类反馈的强化学习
在上一节中,我们看到:如果将一个在相似环境中预训练得到的 Q 值迁移到目标环境中,确实可以有效加快学习过程。比如,把在 6x6 网格、3 个障碍物环境中学到的策略,迁移到 8x8 网格、5 个障碍物的目标环境中,就能显著提升学习效率。
不过,这里有一个重要前提:源环境必须与目标环境足够相似,迁移过去的策略才能真正发挥作用。如果两个环境差异很大,甚至更糟——目标本身是冲突的(例如奖励激励方向都不同),那么迁移学习可能不仅无效,甚至会带来负面影响。
你可以自己尝试修改障碍物和目标位置,让它们与最初训练 Q 值时使用的环境差异更大,观察迁移学习效果会怎样下降。
那么问题来了:在这种迁移学习帮不上太多忙的情况下,我们还能怎么加速训练?
一种办法,就是把人类反馈(Human Feedback) 引入策略训练过程。
人类反馈的几种形式
人类反馈可以有多种形态,比如:
显式奖励(Explicit rewards)
人类直接根据智能体的动作给出奖励或惩罚。
例如,在 grid-world 场景中,如果智能体正在朝着目标前进,人类可以给予正奖励;如果它撞上障碍物,或者明显做了糟糕动作,人类可以给予负奖励。
评价(Evaluations)
人类不一定直接给数值奖励,也可以对动作或策略给出评价。
例如:
- “这个动作不错”
- “你应该往左,而不是往右”
- 或者更简单的点赞 / 点踩、yes / no
示范(Demonstrations)
人类直接展示期望行为,让智能体看到“正确做法”是什么。
这种反馈可以表现为一条完整轨迹,或者是一系列成功完成任务的动作序列。
人类反馈如何帮助学习
智能体会利用这些来自人类的反馈来改进自己的决策过程。具体可以采用多种算法来高效吸收这些反馈。
在 grid-world 的例子里,如果智能体在网格中走了一步,而这一步是“好动作”,人类就可以给它一个正反馈(奖励);如果这一步是“坏动作”,就给它一个负反馈(惩罚)。
与其完全依赖自己在环境中盲目探索来学策略,智能体现在还可以借助人类提供的反馈来更新策略:
- 正反馈会鼓励它在类似情境下重复类似动作
- 负反馈会引导它避免再次采取这些动作
随着这种基于人类反馈的迭代不断进行,智能体会逐步优化策略,从而在 grid-world 中做出更优决策。
在 grid-world 中加入人类反馈
为了直观展示人类反馈如何加速学习,我们继续沿用 grid-world 问题,并在 GridWorld 类中新增一个方法:q_learning_with_human_feedback。这是对前面 q_learning 方法的一个改造版本。
这个方法额外接收一个 human_feedback 输入,它是一个字典,用来记录:在人类看来,从某个当前状态转移到某个下一状态时,应该额外得到多少奖励。
例如:
- 如果智能体当前在
(2, 2),下一步走到(2, 3),而这一步更接近目标(5, 5),那么人类可能给予+1 - 相反,如果智能体当前在
(3, 4),下一步走到(2, 4),这让它离目标更远了,那么人类可能给予-1
这个“人类反馈奖励”会被加到原本 Q-learning 自带的奖励之上。
下面是方法定义:
def q_learning_with_human_feedback(
self, num_episodes=1000, learning_rate=0.1, discount_factor=0.9,
epsilon=0.1, policy_checkpoints=None, human_feedback=None
):
由于该方法与前面的 q_learning 有大量重叠,因此主体流程不再重复展开。你可以参考前面介绍的 Q-learning 实现。它的主要差异点,出现在“状态转移与奖励分配”之后——也就是在更新奖励时,把人类反馈纳入进来。
实现方式如下:
if human_feedback is not None:
if (
state in human_feedback
and next_state in human_feedback[state]
):
reward += human_feedback[state][next_state]
这里的逻辑很简单:
- 先检查是否提供了
human_feedback - 如果当前状态和下一状态这组转移在人类反馈字典中存在
- 就把对应的人类反馈值直接加到原始奖励上
在这个例子里,我们只是简单地把人类反馈“相加”到奖励里。但实际上,人类反馈可以有很多更复杂的整合方式,下一章我们会看到更多方案。
加入完人类反馈后,后面的 Q 值更新、状态推进、统计与返回逻辑都与普通 Q-learning 相同。
对比普通 Q-learning 与带人类反馈的 Q-learning
为了让二者的对比更直观,我们会复用前面迁移学习部分中的两个绘图函数:
plot_reward_comparison()plot_comparison_metrics()
只是会把图例和标签修改为适合“人类反馈 vs 普通训练”的版本。
下面是完整执行代码。这个例子里,我们手动构造了一个 human_feedback 字典,其中:
- 键是当前状态元组
- 值是一个字典,表示从该状态转移到不同下一状态时对应的反馈值
obstacle_positions = [(2, 3), (3, 1), (4, 3)]
grid_world = GridWorld(
grid_size=(6, 6), obstacle_positions=obstacle_positions)
rewards, steps_per_episode, policy_progress = \
grid_world.q_learning(num_episodes=200, learning_rate=0.1, discount_factor=0.95, epsilon=0.1)
grid_world.plot_policy_progress(policy_progress)
human_feedback = {
# Positive feedback for transitioning from (2, 2) to (2, 3):
(2, 2): {(2, 3): 1},
# Positive feedback for transitioning from (2, 3) to (2, 4):
(2, 3): {(2, 4): 1},
# Positive feedback for transitioning from (2, 4) to (2, 5):
(2, 4): {(2, 5): 1},
# Negative feedback for transitioning from (3, 4) to (2, 4):
(3, 4): {(2, 4): -1},
# Negative feedback for transitioning from (4, 4) to (3, 4):
(4, 4): {(3, 4): -1},
# Positive feedback for transitioning from (4, 5) to (5, 5):
(4, 5): {(5, 5): 1},
}
rewards_hf, steps_per_episode_hf, policy_progress_hf = \
grid_world.q_learning_with_human_feedback(
num_episodes=200, learning_rate=0.1,
discount_factor=0.95, epsilon=0.1, human_feedback=human_feedback
)
grid_world.plot_policy_progress(policy_progress)
grid_world.plot_rewards_comparison(rewards_hf, rewards)
grid_world.plot_comparison_metrics(
rewards_hf, rewards, steps_per_episode_hf,steps_per_episode)
图 1.10 展示了普通 Q-learning 下,智能体在 6x6 网格中的若干代表性 episode 轨迹。可以看出:
- 第 2 和第 20 个 episode 时,智能体仍然在摸索策略
- 到第 100 个 episode 时,才明显走出了最优路径,表明它已经学会了目标行为
图 1.10——普通 Q-learning 在 6x6 网格中的若干 episode 轨迹
而对比普通 Q-learning 与加入人类反馈后的 Q-learning 在网格上的策略演化(图 1.10 与图 1.11)就会发现:在加入人类反馈后,智能体几乎已经可以直接采取最优策略。
图 1.11——加入人类反馈后的 Q-learning 在 6x6 网格中的若干 episode 轨迹
如果进一步看总奖励随 episode 的变化,差异会更加明显。图 1.12 展示了总奖励表现的对比:
- 普通 Q-learning 虽然也能在一个合理的训练轮数内逐步达到可接受的奖励并完成收敛
- 但加入人类反馈后,总奖励几乎在第 2 个 episode 就已经收敛
需要注意的是,图 1.12 中的曲线同样做了平滑处理:每 10 个 episode 做一次平均。
图中比较的是:
- 加入人类反馈的 Q-learning(深色线)
- 普通 Q-learning(浅色线)
结果表明:
- 加入人类反馈后,训练在 2 个 episode 左右就收敛
- 普通 Q-learning 则需要 18 个 episode
同时,总训练奖励也更高:
- 带人类反馈:
1022 - 不带人类反馈:
-549
同样,图中没有绘制 episode 0 的初始策略;并且即便加了人类反馈,智能体依旧在使用 epsilon-greedy,因此训练过程中仍然包含探索行为。这里带人类反馈时的总奖励,是“Q-learning 原始奖励 + 人类额外给出的奖励(+1 或 -1)”的总和。
图 1.12——加入人类反馈的 Q-learning(深色)与普通 Q-learning 在平滑总奖励曲线上的对比
图 1.13 则给出了更细粒度的对比:
- 左图:每个 episode 的原始总奖励
- 右图:每个 episode 到达目标所需步数
图 1.13——加入人类反馈的 Q-learning(深色/蓝色更平)与普通 Q-learning,在原始总奖励与到达目标步数上的对比
从图中可以看到,在这个例子里,加入人类反馈之后,智能体几乎立刻就学会了如何用更少步数到达目标状态。蓝色那条更平的曲线中偶尔出现的一些峰谷,说明探索—利用机制仍然在发挥作用,智能体依然会偶尔“试探”一些动作;但从整体平均趋势来看,加入人类反馈后:
- 奖励更高
- 达到目标所需步数更少
- 学习速度明显更快
总结
在本章中,我们介绍了强化学习的核心概念,用一个简单的 grid-world 例子帮助你理解 RL 的关键思想,并借助最基础的算法之一——Q-learning——展示了 RL 是如何工作的。随后,我们又进一步说明了:迁移学习和人类反馈如何帮助智能体加速学习过程。
当然,这些内容更多还是一个入门性的起点。在更复杂的问题中,Q 值未必还能像这里这样用表格存储;很多时候,你需要用神经网络或其他函数逼近器来表示价值函数。与此同时,当状态—动作空间变成连续空间时,Q-learning 也会变得不稳定,这时就需要引入其他类型的策略学习算法。
这些更复杂的情况,我们会在后续章节继续展开。
另外也要注意,虽然本章中我们通过“人类显式给奖励”的方式成功加速了学习,但在大规模场景下,这样做的成本可能会很高,因此往往需要专门的工具与机制来高效地收集和整合人类反馈。关于这一点,后续章节也会深入讨论。
但在进入那些更系统的内容之前,下一章我们会先从更一般的角度,理解人类反馈在强化学习中的角色。
参考资料
如果你希望进一步深入 RL,下面这些书籍、课程、论文和资源都很值得参考:
- Sutton & Barto,《Reinforcement Learning: An Introduction》
http://incompleteideas.net/book/the-book-2nd.html - David Silver 的强化学习课程
https://www.davidsilver.uk/teaching/ - Berkeley CS 285
https://rail.eecs.berkeley.edu/deeprlcourse/ - OpenAI Spinning Up 文档
https://spinningup.openai.com/en/latest/user/introduction.html
本章代码示例中使用到的可视化库 Matplotlib,其官方文档如下:
- Matplotlib Documentation 3.8.4
https://matplotlib.org/stable/index.html