ML-For-Beginners 强化学习实战:用 Q-Learning 训练 Peter 在《彼得与狼》网格世界中寻路
【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners
本篇技术指南以 ML-For-Beginners 课程第 8 部分《强化学习》的第一课为核心,完整讲解强化学习(Reinforcement Learning)的三大要素、随机游走基线策略、奖励函数设计、Q 表(Q-Table)的构建与更新,以及 Bellman 方程的推导与落地实现。读完本文,你将能够基于仓库中的rlboard.py环境模块与notebook.ipynb交互式笔记本,从零实现一个可运行的 Q-Learning 智能体,让"彼得"学会在避开狼和水域的前提下,以远短于随机游走的步数找到苹果。
图:强化学习核心概念速览(Sketchnote)
背景:为什么强化学习不同于监督学习
在 ML-For-Beginners 课程中,前面章节已介绍过两类经典问题:监督学习(分类、回归)依赖带标签的数据集提供参考答案;无监督学习(聚类)在无标签数据上发现结构。强化学习则属于第三种范式:不需要标签,而是让Agent(智能体)在Environment(环境)中反复实验,通过Reward Function(奖励函数)定义的目标来学习最优行为。
以电脑游戏为例:要让程序学会下棋或玩《超级马里奥》,我们并没有覆盖海量状态-动作对的数据集,因此无法把它当作分类问题。强化学习的思路是"让程序自己玩很多次并观察结果",这需要两样东西:
- 环境与模拟器:定义游戏规则、所有可能的状态与动作;
- 奖励函数:告诉智能体每一步或每一局表现得怎么样。
强化学习的关键特性在于:通常在整局游戏结束时我们才知道输赢,中间某个单独的动作很难立即判断好坏,奖励往往在最后才给出。本课将用经典的无模型算法Q-Learning来应对这种不确定性。
环境:Peter 的 8×8 网格世界
本课的故事背景取自俄国作曲家普罗科菲耶夫的交响童话《彼得与狼》。为简化问题,Peter 的世界被建模为一个width × height的正方形棋盘,本课使用 8×8 尺寸。棋盘上每格可能是五种状态之一:
- 地面:Peter 和其他生物可以行走;
- 水域:显然不能行走;
- 树或草地:可以休息的地方;
- 苹果:Peter 需要找到的食物;
- 狼:危险,必须避开。
与棋盘交互的全部代码封装在独立的 Python 模块 rlboard.py 中。从源码看(rlboard.py),环境由Board类实现,其内部用numpy矩阵存储各格类型,Cell枚举依次为empty / water / wolf / tree / apple,并通过randomize()方法以可控的随机种子生成水域、狼、树和苹果的布局。Board还提供了at()(查询当前格)、is_valid()(判断坐标是否越界)、move()/move_pos()(移动与坐标计算)以及plot()(绘制棋盘)等基础方法,它们是后面所有策略与学习算法的基础设施。
在 notebook.ipynb 中创建并绘制示例棋盘(代码块 1):
from rlboard import * width, height = 8,8 m = Board(width,height) m.randomize(seed=13) m.plot()提示:如果从云端(如在线 Notebook 环境)运行代码,需要把 rlboard.py 一并获取到与 Notebook 相同的目录,因为它被 Notebook 代码直接引用。
图:
Board.randomize(seed=13)生成的示例环境,蓝色为水域、绿色为树木草地、苹果为奖励点、狼需要避开
动作与策略(Policy)
Peter 的目标是找到苹果,同时避开狼和其他障碍。在任何位置,他可以在四个方向中选择一个动作:上(U)、下(D)、左(L)、右(R)。用字典把动作映射为坐标增量,例如向右移动R对应(1,0)(代码块 2):
actions = { "U" : (0,-1), "D" : (0,1), "L" : (-1,0), "R" : (1,0) } action_idx = { a : i for i,a in enumerate(actions.keys()) }由此引出两个核心概念:
- 策略(Policy):智能体(Peter)的决策方式,是一个"在给定状态下返回动作"的函数。在本例中,状态由棋盘布局加当前玩家位置共同表示;
- 强化学习的目标:通过学习获得一个"好"的策略,从而高效解决问题。作为基线,我们先考虑最简单的策略——随机游走(Random Walk)。
基线策略:随机游走
随机游走策略会在每一步从允许的动作中随机选择一个,直到到达苹果(代码块 3):
def random_policy(m): return random.choice(list(actions)) def walk(m,policy,start_position=None): n = 0 # number of steps # set initial position if start_position: m.human = start_position else: m.random_start() while True: if m.at() == Board.Cell.apple: return n # success! if m.at() in [Board.Cell.wolf, Board.Cell.water]: return -1 # eaten by wolf or drowned while True: a = actions[policy(m)] new_pos = m.move_pos(m.human,a) if m.is_valid(new_pos) and m.at(new_pos)!=Board.Cell.water: m.move(a) # do the actual move break n+=1 walk(m,random_policy)walk函数返回路径长度,成功找到苹果返回正数步数,被狼吃掉或掉进水里返回-1。注意内部循环的逻辑:策略选出的动作若导致非法位置(越界或踏入水域)会被拒绝,直到选出合法动作才真正移动——这一点对应 rlboard.py 中Board.walk的实现思路。
把实验重复 100 次并统计(代码块 4):
def print_statistics(policy): s,w,n = 0,0,0 for _ in range(100): z = walk(m,policy) if z<0: w+=1 else: s += z n += 1 print(f"Average path length = {s/n}, eaten by wolf: {w} times") print_statistics(random_policy)结果很有说服力:随机游走的平均路径长度约为 30-40 步,而最近苹果的平均距离只有 5-6 步——大量步数被浪费在反复探索与兜圈子上。这清楚地说明,仅仅随机碰运气远远不够,我们需要让策略"变聪明"。
设计奖励函数
要比较"哪个动作更好",必须先明确目标。目标用奖励函数(Reward Function)形式化:它为每个状态返回一个分数,数值越高代表奖励越好(代码块 5):
move_reward = -0.1 goal_reward = 10 end_reward = -10 def reward(m,pos=None): pos = pos or m.human if not m.is_valid(pos): return end_reward x = m.at(pos) if x==Board.Cell.water or x == Board.Cell.wolf: return end_reward if x==Board.Cell.apple: return goal_reward return move_reward该函数体现了两个重要设计点:
- 稀疏的实质奖励:大幅度的奖励(+10 到达苹果、-10 进入水域/遇狼)只在"终局"状态给出,普通移动每步只有 -0.1 的微小代价;
- 隐式引导:正是因为大部分状态没有显著奖励,算法必须"记住"那些最终导向正奖励的好步骤并提高其权重,同时抑制导向坏结果的动作。这就是后文 Q 表需要承担的记忆职责。
Q-Learning:用 Q 表记录"动作的好坏"
Q-Learning用一个称为Q 表(Q-Table)的函数或数据结构来定义策略,它记录"在给定状态下每个动作有多好"。由于棋盘尺寸为width × height,Q 表可以很方便地表示为一个形状为width × height × len(actions)的 numpy 数组(代码块 6):
Q = np.ones((width,height,len(actions)),dtype=np.float)*1.0/len(actions)初始时所有 Q 值都被设为相等值 0.25,这等价于"随机游走"策略——每个状态下所有动作一样好。把 Q 表传给plot函数可以把它可视化在棋盘上:m.plot(Q)。在每格中心有一个"箭头"指示偏好移动方向,由于初始所有方向等价,显示为圆点。
图:初始化后的 Q 表,各方向等价,格内显示为圆点
接下来需要运行模拟、探索环境,并学习出更优的 Q 值分布,让 Peter 更快找到苹果。
Bellman 方程:Q-Learning 的本质
一旦开始移动,每个动作都有对应奖励,理论上可以按"立即奖励最高"来选下一步。但多数状态下这一步并不能直接达成找到苹果的目标,所以我们无法立即判断哪个方向更好。
记住:重要的不是立即结果,而是模拟结束时得到的最终结果。
为处理这种延迟奖励,需要借助动态规划的思想递归地看待问题。假设当前处于状态s,要转移到下一状态s',我们会获得由奖励函数定义的立即奖励r(s,a),加上某种"未来奖励"。如果假设 Q 表已正确反映每个动作的"吸引力",那么在状态s'处我们会选择使Q(s',a')最大的动作a。于是,状态s处能获得的最佳未来奖励为maxa'Q(s',a')(对状态s'下所有可能动作求最大值)。
由此得到计算状态s、动作a的 Q 值的Bellman 公式:
其中 γ 是折扣因子(Discount Factor),它决定了我们更偏好当前奖励还是未来奖励:γ 越接近 1,未来奖励的权重越大;越接近 0,越"短视"。
学习算法伪代码
基于 Bellman 方程,可以写出学习算法的伪代码:
- 用相等数值初始化 Q 表 Q(覆盖所有状态与动作)
- 设置学习率 α ← 1
- 重复多次模拟:
- 从随机位置开始
- 重复执行:
- 在状态s选择一个动作a
- 执行动作,转移到新状态s'
- 若遇到终局条件,或累计奖励过小——退出本次模拟
- 计算新状态下的奖励r
- 按 Bellman 方程更新 Q 函数:Q(s,a)←(1-α)Q(s,a)+α(r+γ maxa'Q(s',a'))
- s←s'
- 更新累计奖励并减小 α
可以看到,α(学习率)控制新旧信息的混合比例:α 越大,新经验对 Q 值的修正越激进;α 随训练逐渐衰减,能让训练后期趋于稳定。
探索与利用的平衡(Exploit vs. Explore)
在上面的伪代码中,步骤 2.1"如何选择动作"并未限定:
- 纯随机选择= 随机探索环境,容易频繁"死亡",也会访问平时不会去的地方;
- 纯贪婪利用= 始终选择 Q 表中值最高(最好)的动作,利用已知知识,但会阻止探索其他状态,很可能找不到最优解。
最佳做法是在两者间取得平衡:以与 Q 表数值成比例的概率选择动作。训练初期 Q 值全部相同,这等价于随机选择;随着对环境的了解加深,智能体更可能沿最优路线前进,同时偶尔仍会尝试未探索的路径。
Python 实现:5000 次 epoch 的完整训练
实现学习算法前,需要一个把 Q 表中任意数值转换成对应动作概率向量的辅助函数(代码块 7):
def probs(v,eps=1e-4): v = v-v.min()+eps v = v/v.sum() return vprobs先减去最小值并加上极小的eps,目的是避免初始情况下(向量各分量完全相同)出现除以 0 的问题,然后归一化为概率分布。
接着运行 5000 次实验(epochs)进行学习(代码块 8):
for epoch in range(5000): # Pick initial point m.random_start() # Start travelling n=0 cum_reward = 0 while True: x,y = m.human v = probs(Q[x,y]) a = random.choices(list(actions),weights=v)[0] dpos = actions[a] m.move(dpos,check_correctness=False) # we allow player to move outside the board, which terminates episode r = reward(m) cum_reward += r if r==end_reward or cum_reward < -1000: lpath.append(n) break alpha = np.exp(-n / 10e5) gamma = 0.5 ai = action_idx[a] Q[x,y,ai] = (1 - alpha) * Q[x,y,ai] + alpha * (r + gamma * Q[x+dpos[0], y+dpos[1]].max()) n+=1这段训练循环的要点:
- 动作选择:
random.choices(..., weights=v)按 Q 值归一化概率抽样,实现探索/利用平衡; - 回合终止:
m.move(dpos, check_correctness=False)允许 Peter 走出棋盘以终止回合(对应end_reward);累计奖励低于 -1000 同样终止,防止无限游荡; - 学习率衰减:
alpha = np.exp(-n / 10e5)随步数指数衰减,越到后期对 Q 值的修正幅度越小,对应伪代码中"减小 α"; - Q 值更新:直接实现 Bellman 公式
(1-α)Q + α(r + γ·max(Q[s']),其中gamma = 0.5是本课的折扣因子取值; - 路径记录:每个回合结束把步数
n追加到lpath,供后面分析学习过程使用。
执行结束后,Q 表被更新为能反映"每个状态下不同动作吸引力"的数值。把它可视化,每格画一个指向偏好移动方向的向量(为简化,用圆点代替箭头头):
图:5000 个 epoch 训练后可视化的 Q 表,白色格子内的箭头指示各状态的偏好移动方向
策略检查:直接用 Q 表导航
由于 Q 表列出了每个状态下每个动作的"吸引力",用它定义高效导航非常简单。最直接的做法是选择 Q 值最高的动作(代码块 9):
def qpolicy_strict(m): x,y = m.human v = probs(Q[x,y]) a = list(actions)[np.argmax(v)] return a walk(m,qpolicy_strict)尝试几次后你可能发现,代码有时会"卡住",需要按 Notebook 的 STOP 按钮中断。这是因为可能出现两个状态在最优 Q 值上"互相指向"的情况,导致智能体在这两个状态间无限来回。
这正是随机游走与简单贪婪策略都存在的缺陷,也引出了两道动手练习(Challenge):
任务 1:修改
walk函数,把路径最大长度限制在一定步数(例如 100),观察上述代码偶尔会返回这个上限值。任务 2:修改
walk函数,使其不返回此前已经访问过的位置。这能防止walk死循环,但智能体仍可能被困在无法逃脱的角落。
更好的导航:训练时所用的探索/利用混合策略
更优的导航策略是训练时使用的"按 Q 值比例随机选择"(代码块 10):
def qpolicy(m): x,y = m.human v = probs(Q[x,y]) a = random.choices(list(actions),weights=v)[0] return a print_statistics(qpolicy)该策略虽然仍可能让智能体回到已探索过的位置,但运行print_statistics(内部模拟 100 次)后,平均路径长度会显著缩短,通常在 3-6 步之间,相比随机游走的 30-40 步是数量级的提升。这说明 Q-Learning 学到的知识已经能稳定指引 Peter 高效觅食。
观察学习过程:路径长度曲线说明了什么
学习过程本质上是探索与利用的动态平衡。绘制每个 epoch 的路径长度,可以看到一条非常有信息量的曲线:
图:横轴为训练 epoch,纵轴为该回合路径长度,用于观察学习进程
曲线的三个阶段值得仔细解读:
- 平均路径长度先上升:对环境一无所知时,智能体很容易被困在坏状态(水域或狼附近)并快速结束回合;随着知识积累,它能探索更久,但此时还不知道苹果确切位置,所以路径反而变长;
- 路径长度随后下降:学到足够多知识后,智能体更容易达成目标,路径开始变短。但由于仍保留探索,它经常偏离最优路线去尝试新选项,所以路径比最优略长;
- 长度偶发骤增:曲线某处可能突然跳高,体现了过程随机性——新更新的 Q 值可能在某个时刻"破坏"了之前学到的系数。理想情况下应通过降低学习率来抑制(例如训练末期只对 Q 值做微小调整,本课代码中
alpha = np.exp(-n / 10e5)正是这种衰减思想的体现)。
超参数:决定学习质量的关键
学习过程的成败与质量高度依赖学习率(learning rate)、学习率衰减(learning rate decay)和折扣因子(discount factor)。这些参数被称为超参数(Hyperparameters),以区别于训练中被优化的参数(Parameters)(例如 Q 表系数)。寻找最佳超参数取值的过程称为超参数优化(Hyperparameter Optimization),是一个值得单独探讨的主题——在扩展练习中你会真切体会到调参的影响。
扩展练习:更真实的世界
本课配套作业 assignment.md 要求把世界改得更真实:Peter 移动会消耗能量并累积疲劳;吃苹果可恢复能量;在树下或草地上休息可消除疲劳;他还需要找到并击败狼,而只有能量和疲劳达到一定水平才能赢下战斗。此时状态不再只是棋盘位置,还包含能量与疲劳,需要扩展状态表示(如(Board, energy, fatigue)元组或派生自Board的状态类)。
仓库中的参考答案 solution/assignment-solution.ipynb 展示了如何用state类封装这些信息,并将训练 epoch 提升至 10000、把学习率衰减调整为alpha = np.exp(-n / 3000)(见 solution/notebook.ipynb)。由于"打赢狼"是稀有事件,作业明确指出:你可能需要调整超参数、尤其是 epoch 数量,训练时间会显著变长。完成作业后,应保留随机游走代码并与你的 Q-Learning 算法在"胜/负场次"上做对比。
小结
通过这一课,你完成了从环境建模、基线策略到 Q 表学习、策略评估的完整闭环:Board环境(rlboard.py)提供了可复现的网格世界;奖励函数把"找苹果、躲狼、避水"的目标数字化;Bellman 方程与 α、γ 两个超参数驱动 Q 表不断更新;探索/利用平衡让平均路径从 30-40 步降到 3-6 步。沿着本仓库课程主线继续深入,下一课 2-Gym 会把同样的 Q-Learning 思想应用到 OpenAI Gym 的 CartPole 连续状态问题中——把本课打下的基础迁移过去,你将看到强化学习方法论在不同问题上的统一威力。
【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考