ML-For-Beginners 强化学习实战:在 Q-Learning 中构建带能量与疲劳机制的“真实世界”并重构奖励函数
【免费下载链接】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/1-QLearning一节的课后作业“一个更真实的世界(A More Realistic World)”为核心,讲解如何把“彼得与狼”的简易强化学习环境改造成一个需要消耗能量、累积疲劳、进食与休息、最终击败饿狼的复杂环境。读完本文,你将掌握:如何把游戏规则翻译成状态与奖励函数、如何在既有 rlboard.py 环境之上扩展实现、如何调整 Q-Learning 的超参数应对“稀有成功事件”,以及如何用胜负次数这一指标公平地对比随机游走与学习策略。仓库中英文原版作业位于 8-Reinforcement/1-QLearning/assignment.md,本节作业的孟加拉语译本即 translations/bn/8-Reinforcement/1-QLearning/assignment.md。
在原始课程环境中,Peter 可以“几乎不饿不累”地四处走动,目标只是找到苹果并避开狼。这个作业要求我们补上现实约束:移动要消耗体能、必须进食与休息,并且最终目标从“找到苹果”升级为“找到并击败饿狼”。这也是强化学习从玩具环境走向真实问题的一个典型缩影——状态更复杂、奖励更稀疏、训练更长。
一、任务来源与前置知识
本作业来自“强化学习与 Q-Learning 入门”一课。该课的完整讲义位于 8-Reinforcement/1-QLearning/README.md,配套的可运行入门笔记本为 8-Reinforcement/1-QLearning/notebook.ipynb,环境模拟代码则封装在 8-Reinforcement/1-QLearning/rlboard.py。
理解作业前需要先掌握三个强化学习基本概念:
- Agent(智能体):本任务中就是 Peter。
- State(状态):智能体当前所处的情况。原世界里基本等价于“Peter 在棋盘上的位置”。
- Action(动作):每个状态下可执行的动作集合,此处是上、下、左、右四个方向的移动。
- Reward(奖励):执行某个动作后环境给出的数值信号,它由奖励函数定义。强化学习的目标,就是在给定奖励函数下学到最优策略(policy),让累计奖励最大。
原世界是一个width × height的方形棋盘,每个格子可以是:空地(ground)、水(water,不可行走)、树或草(tree/grass,可以休息)、苹果(apple,Peter 乐于获取的食物)、狼(wolf,危险源)。这些格子类型在 rlboard.py 中以Board.Cell内部类的方式用整数常量定义:empty = 0、water = 1、wolf = 2、tree = 3、apple = 4。
运行环境代码的方式很简单:先把rlboard.py与笔记本放到同一目录,然后创建并随机化棋盘:
from rlboard import * width, height = 8, 8 m = Board(width, height) m.randomize(seed=13) m.plot()Board.randomize(见 rlboard.py)支持water_size、num_water、num_wolves、num_trees、num_apples、seed等参数,控制棋盘上各类格子的数量与随机种子。
二、原世界的“找苹果”基线:动作集与随机游走
在原世界里,Peter 的四个动作用字典映射到坐标增量:
actions = { "U" : (0,-1), "D" : (0,1), "L" : (-1,0), "R" : (1,0) } action_idx = { a : i for i,a in enumerate(actions.keys()) }最简单的基准策略是随机游走(random walk):每次从合法动作里随机挑一个,直到走到苹果(成功)或踩到狼/水(失败)。原课程的walk实现大致为:
def random_policy(m): return random.choice(list(actions)) def walk(m, policy, start_position=None): n = 0 # 步数 if start_position: m.human = start_position else: m.random_start() while True: if m.at() == Board.Cell.apple: return n # 成功 if m.at() in [Board.Cell.wolf, Board.Cell.water]: return -1 # 被狼吃掉或溺水 # 反复取动作直到落在合法且非水的格子上 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) break n += 1walk返回的是路径长度;用print_statistics类函数跑 100 次后,可以统计出“平均路径长度”与“被狼吃掉次数”。原课程讲义指出,随机游走找到苹果的平均路径长度约为 30~40 步,而最近苹果的平均直线距离只有 5~6 步,说明纯随机策略相当低效。
这就是本作业要改写的“旧世界”基线:目标 = 找到苹果;失败 = 遇狼或落水。
三、新世界的游戏规则(作业核心内容)
作业要求按照以下五条规则把世界改得更真实(下文为对原文规则的中文完整转述与解读):
- 移动消耗体能:Peter 每从一个地方移动到另一个地方,都会损失一定能量(energy),并累积一定疲劳(fatigue)。
- 吃苹果补充能量:Peter 可以通过吃苹果获得更多能量。
- 在树下或草地上休息可消除疲劳:走进棋盘上包含树或草(即绿色区域)的格子,Peter 可以摆脱疲劳。
- 目标变为击杀狼:Peter 需要找到狼并把它杀死。
- 战斗有门槛:要杀死狼,Peter 的能量与疲劳必须达到一定水平,否则他会在战斗中落败。
与原世界相比,最本质的三点变化是:
- 动作有了代价:移动不再是“免费”的,任何移动都会让能量下降、疲劳上升,因此绕路、迷路都要付出真实代价;
- 可恢复资源:苹果(能量)与树/草地(疲劳清零)成为需要主动规划去获取的“补给点”,这使策略不再只是“几何上最短路径”;
- 终局条件改变:成功判定从“到达苹果格”变成“以足够好的体能状态站到狼格上并赢得战斗”,吃苹果、休息的最终目的都是为这一战做准备。
四、为什么这会让问题变难:状态膨胀与稀疏奖励
在原世界中,状态 ≈ Peter 的位置,状态空间大小约为width × height。加入能量与疲劳后,状态还包含“能量级别”和“疲劳级别”,因此理论上完整的状态空间是 (棋盘位置, 能量, 疲劳) 的三元组合。
作业原文也明确给出了三种可行表示:
- 用元组
(Board, energy, fatigue)表示状态; - 为状态定义一个新类(可以从
Board派生); - 甚至直接修改 rlboard.py 中的原始
Board类。
难度上升还体现在奖励的稀疏性上:原世界中“到达苹果”在随机游走下尚能在几十步内发生;而新世界中“与狼战斗并获胜”属于小概率事件,大多数回合可能在到达狼之前就因为能量耗尽、被水围困等原因提前结束。正如作业注释所警告的——因为游戏成功(与狼战斗)是稀有事件,你可能需要更长的训练时间。
五、从源码看Board可以复用什么能力
在动手前,先盘点 rlboard.py 中可直接复用的机制:
| 能力 | 方法 / 字段 | 源码位置 | 作用 |
|---|---|---|---|
| 格子类型 | Board.Cell.empty/water/wolf/tree/apple | rlboard.py#L44-L49 | 区分空地、水、狼、树/草、苹果 |
| 读取当前格 | Board.at(pos=None) | rlboard.py#L99-L103 | 返回 Peter 当前(或指定位置)的格子类型 |
| 越界判断 | Board.is_valid(pos) | rlboard.py#L105-L106 | 判断坐标是否在棋盘内 |
| 坐标增量 | Board.move_pos(pos, dpos) | rlboard.py#L108-L109 | 计算移动后的新坐标 |
| 实际移动 | Board.move(dpos, check_correctness=True) | rlboard.py#L111-L114 | 更新human位置 |
| 随机起点 | Board.random_start() | rlboard.py#L121-L126 | 随机把 Peter 放到一个空地上 |
| Peter 位置 | Board.human | rlboard.py#L64 | 记录当前坐标(x, y) |
| 棋盘数据 | Board.matrix | rlboard.py#L54 | numpy数组,存每格类型 |
可以看到:位置移动、越界判定、随机起点这些机制都是现成的;能量与疲劳并不存在于matrix中,它们更像是一回合内随时间演化的“回合变量”,因此推荐用第 4 节提到的方案一或方案二——用一个封装state去同时携带board、energy、fatigue。
仓库的示例解答笔记本 8-Reinforcement/1-QLearning/solution/assignment-solution.ipynb 正是采用了“封装类”路线。它定义了一个state类,其关键结构如下(示例,来源于仓库 solution 目录):
class state: def __init__(self, board, energy=10, fatigue=0, init=True): self.board = board self.energy = energy self.fatigue = fatigue self.dead = False if init: self.board.random_start() self.update() def at(self): return self.board.at() def update(self): if self.at() == Board.Cell.water: self.dead = True return if self.at() == Board.Cell.tree: self.fatigue = 0 # 树/草地上休息:疲劳清零 if self.at() == Board.Cell.apple: self.energy = 10 # 吃苹果:能量补满 def move(self, a): self.board.move(a) self.energy -= 1 # 规则 1:移动耗能 self.fatigue += 1 # 规则 1:移动累积疲劳 self.update() def is_winning(self): return self.energy > self.fatigue # 规则 5:能量压过疲劳才能赢这段代码可以看作五条新规则的“最小可运行翻译”:初始能量10、初始疲劳0;每移动一步能量-1、疲劳+1;踏上树/草地时疲劳清零;吃到苹果时能量回满(示例选择把苹果与休息作为“状态刷新点”,用复位到满值实现补给);is_winning()用energy > fatigue表示“具备击杀狼的体能条件”。
需要说明:以上数值(初始能量 10、疲劳上限的复位策略、
energy > fatigue的判胜条件)只是仓库示例解答的一种具体取值。作业本身并未限定参数,允许你自行定义“某程度的能量与疲劳”判定,这正是评分标准里“世界规则重新定义”的自由度所在。
六、重写奖励函数:把“能量 − 疲劳”变成每步的形状奖励
原课程的奖励函数(见课程代码块 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新世界里这套函数不再适用,因为“终点是苹果”已不是目标。作业要求**“按照游戏规则修改上面的奖励函数”。仓库示例解答给出了一种很自然的改造:用energy - fatigue作为“每一时刻的体能净额”,并把它当作连续的形状奖励(shaping reward)**;同时用大额正负分刻画终局事件:
def reward(s): r = s.energy - s.fatigue if s.at() == Board.Cell.wolf: return 100 if s.is_winning() else -100 # 打赢 +100,打输 -100 if s.at() == Board.Cell.water: return -100 # 溺水结束 return r # 其余情况:能量净额即即时回报这种设计的直觉是:
energy - fatigue越大,说明 Peter 状态越好,因此策略会自发学会“多吃苹果(抬高能量)、多去树/草地休息(压低疲劳)”;- 所有通往狼的战斗都发生在状态评估之后:若在狼格上
is_winning()为真则得到极大的正奖励100,否则得到极大的负奖励-100,形成清晰的稀疏终局信号; - 落水给
-100,把“走进水里导致提前出局”也当作失败终局。
你也可以自行发挥:比如用分段函数区分普通移动、吃苹果、休息的即时奖励,或对能量阈值做更细的判定。关键在于——奖励函数必须严格反映第 3 节列出的五条规则,否则 Q-Learning 无从学到正确的因果链(这正是作业评分中“奖励函数未完全定义 → Needs Improvement”的扣分点)。
七、训练循环的适配与超参数调整
Q-Learning 的骨架不变,核心仍是贝尔曼方程驱动 Q-Table 更新:
Q(s,a) ← (1 − α)·Q(s,a) + α·(r + γ·max_a' Q(s',a'))其中 α 是学习率、γ 是折扣因子。原课程用 5000 个 epoch(回合)训练,代码片段大致如下(probs用于把 Q 值转成选择各动作的概率):
for epoch in range(5000): m.random_start() # 随机起点 n, cum_reward = 0, 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) # 允许走出棋盘以终止回合 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在新世界中,仓库示例解答solution/assignment-solution.ipynb保持了同样的 Q-Table 更新骨架,但做了四处关键调整:
- 回合对象从
m变为state:每个 epoch 用s = state(m)开始,之后所有判断都基于封装了能量/疲劳的状态对象。 - Epoch 数量翻倍:示例使用
for epoch in range(10000),印证了作业“成功事件稀有、训练需更久”的提示。 - 动作采样前先过滤非法移动:示例循环内反复抽样,直到
board.is_valid(move_pos(human, dpos))成立,避免把能量白白浪费在越界动作上。 - 学习率衰减更激进:示例采用
alpha = np.exp(-n / 3000)(n 为当前回合内步数),gamma仍为0.5。
需要强调:这些超参数(epoch 数、α 的衰减速率、γ 的取值)不是唯一正确答案。作业注释明确指出“你可能需要调整超参数尤其是 epoch 数量才能让它工作”。调参时值得留意的经验包括:
- 若回合普遍很长,说明策略常在迷路兜圈,可考虑提高学习率或加强休息/吃苹果的奖励信号;
- 若狼战几乎不发生(比如一直落水或绕不开水),需要增大 epoch、或改进动作选择策略让智能体有更多机会靠近狼;
- 折扣因子 γ 越小,智能体越“短视”;γ 越大,越看重远期收益——杀狼属于远期事件,适度偏大的 γ 更合理。
probs函数的细节也值得注意——把 Q 向量转化为概率前加上极小值eps(如1e-4),是为了避免初始化阶段各 Q 值完全相同时除以零(见课程代码)。
八、如何统计并对比胜负:随机游走 vs Q-Learning
作业还特别要求:保留负责随机游走策略的代码,并在最后把你的算法结果与随机游走进行对比。对比的指标是“赢的局数”与“输的局数”。统计函数可以仿照示例解答写成这样:
def print_statistics(policy): s, w, n = 0, 0, 0 for _ in range(100): z = walk(m, policy) if z < 0: w += 1 # 被狼杀死 elif z == 0: n += 1 # 溺水 else: s += 1 # 赢下与狼的战斗 print(f"Killed by wolf = {w}, won: {s} times, drown: {n} times")需要注意示例中对回合结束的约定:随机游走版本的walk内部,当站在狼格上时按state.is_winning()判断胜败——胜利返回正步数n,战败返回-n;走进水里返回0(溺水)。这一约定让三种结局(赢 / 被狼杀 / 溺水)能够被一条统计函数区分开。
仓库的solution/assignment-solution.ipynb中保留了运行时输出(execution output),可作为“参考结果”观察。在固定种子13的棋盘上,随机游走 100 次的记录是:
Killed by wolf = 5, won: 1 times, drown: 94 times同样的棋盘、同样 100 次统计,改用训练 10000 个 epoch 之后的 Q-Learning 策略(按 Q 值概率采样,见下述qpolicy),记录为:
Killed by wolf = 1, won: 9 times, drown: 90 timesdef qpolicy(m): x, y = m.human v = probs(Q[x, y]) a = random.choices(list(actions), weights=v)[0] return a print_statistics(qpolicy)解读这份参考输出时要客观:Q-Learning 策略把“战胜狼”的次数从 1 次提升到 9 次、被狼击杀从 5 次降到 1 次,说明学到的策略确实更倾向于“为狼战做准备”;但 100 次里仍有 90 次是溺水结束。这并不奇怪——大部分回合可能根本没走到狼所在区域,印证了“与狼战斗是稀有事件”的原始提示。这些数字仅代表该示例解答在某一随机种子下的单次运行结果,不应理解为普适结论;换棋盘种子、改超参数都会得到不同数字,作业评分的重点在于 Q-Learning 是否比随机游走赢得更多。
在训练完成后,还可以用m.plot(Q)可视化 Q-Table 学习结果,观察每个格子上箭头的偏好方向(初始时所有方向等概率,学习后箭头会指向更有利的移动)。
九、技术要点归纳:一条可复现的解题路径
综合上述分析,完成本作业的推荐路径可归纳为五步:
- 保留基线:复制随机游走策略与
print_statistics统计代码,作为对照基准; - 定义状态容器:用元组
(board, energy, fatigue)或封装类承载棋盘 + 能量 + 疲劳,按五条规则实现移动、补给、休息与死亡逻辑; - 重写奖励函数:让奖励反映每步的能量净额,并给“战胜狼 / 战败 / 溺水”配置差异明显的终局大额奖励;
- 跑更长的 Q-Learning:把 epoch 数量加大(示例为 10000),必要时调整 α 衰减与 γ,让稀有事件有足够机会被采样到;
- 按胜负次数对比:在同一棋盘上用同一统计函数跑随机游走与 Q-Learning,比较赢/输/溺水三类次数。
期间有两条自查线索可以随时核对:
- 随机游走应“偶尔”能赢——如果 100 次里一次战斗都没发生,先检查状态更新逻辑(例如是否从未真正踏上过狼格、或判胜条件写反);
- Q-Learning 训练完成后应比随机游走更少落水、更多取胜——如果完全无改善,优先检查奖励函数是否完整覆盖了五条规则,以及 epoch 是否足够长。
十、作业完成度对照:官方评分标准
原作业末尾给出了一张三档评分表(Rubric),是判断“做到什么程度算完成”的权威标准,此处完整转述:
| 标准 | 优秀(Exemplary) | 合格(Adequate) | 需改进(Needs Improvement) |
|---|---|---|---|
| 综合要求 | 提交的笔记本包含新世界规则的完整定义、Q-Learning 算法与必要的文字解释,且 Q-Learning 相对随机游走显著提升了结果 | 提交了笔记本,Q-Learning 已实现并相对随机游走有所改进,但提升不显著;或笔记本文档化不足、代码结构混乱 | 只做了重新定义世界规则的初步尝试,但 Q-Learning 算法不工作,或奖励函数没有被完整定义 |
对照该表做交付前的自检:是否有对五条规则的清晰文字描述?随机游走与 Q-Learning 是否在同一条统计流程下可对比?奖励函数是否把所有终局(胜利/战败/溺水/补给)都覆盖到?笔记本里是否解释了关键设计决策?这四点都做到,就满足“优秀”档的核心要求。
十一、延伸阅读与仓库文件索引
如果你想进一步深入本节的实现细节,可以直接阅读以下仓库文件:
- 本作业英文原版:8-Reinforcement/1-QLearning/assignment.md,孟加拉语译本:translations/bn/8-Reinforcement/1-QLearning/assignment.md
- 课程讲义(含随机游走、Q-Table、贝尔曼方程、explore/exploit 的完整推导):8-Reinforcement/1-QLearning/README.md
- 入门实验笔记本:8-Reinforcement/1-QLearning/notebook.ipynb
- 环境模拟模块(
Board类等):8-Reinforcement/1-QLearning/rlboard.py - 本作业的官方示例解答(含
state类与能量/疲劳奖励实现):8-Reinforcement/1-QLearning/solution/assignment-solution.ipynb
在本地运行笔记本时,请务必让rlboard.py与 notebook 位于同一目录(cloud 环境同样需要先上传该文件),这是课程 README 明确给出的运行前提。此外,你可以按作业要求“把随机游走代码保留在解决方案里”,让两个策略的胜负统计并排出现——这既是评分标准的要求,也是检验学习算法真实价值的科学做法。
【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考